diff --git a/.env.deploy.example b/.env.deploy.example new file mode 100644 index 0000000..d19c4cf --- /dev/null +++ b/.env.deploy.example @@ -0,0 +1,16 @@ +# Deploy script environment variables +PRIVATE_KEY= # Private key of the deployer +DEPLOYER= # Address of the deployer +DEPLOYER_PROXY_NONCE= # Nonce of the deployer when creating the Wrapped M proxy +EXPECTED_PROXY= # Address of the expected Wrapped M proxy +M_TOKEN= # Address of the M token +REGISTRAR= # Address of the Registrar +EXCESS_DESTINATION= # Address of the Excess Destination +SWAP_FACILITY= # Address of the Swap Facility +MIGRATION_ADMIN= # Address of the Migration Admin + +# RPC URL to deploy to +DEPLOY_RPC_URL= + +# Used for verifying contracts on Etherscan +ETHERSCAN_API_KEY= diff --git a/.env.example b/.env.example index 8086609..33b10f2 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,11 @@ # Localhost RPC URL -export LOCALHOST_RPC_URL=http://127.0.0.1:8545 +LOCALHOST_RPC_URL=http://127.0.0.1:8545 # Mainnet RPC URLs -export MAINNET_RPC_URL= +MAINNET_RPC_URL= # Testnet RPC URLs -export SEPOLIA_RPC_URL= +SEPOLIA_RPC_URL= # Used for verifying contracts on Etherscan -export ETHERSCAN_API_KEY= +ETHERSCAN_API_KEY= diff --git a/.env.upgrade.example b/.env.upgrade.example new file mode 100644 index 0000000..c613760 --- /dev/null +++ b/.env.upgrade.example @@ -0,0 +1,8 @@ +# Mainnet upgrade script environment variables +PRIVATE_KEY= # Private key of the deployer + +# Mainnet RPC URL to perform upgrade +MAINNET_RPC_URL= + +# Used for verifying contracts on Etherscan +ETHERSCAN_API_KEY= diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 74b90b2..735bade 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,7 +32,7 @@ jobs: run: make coverage - name: Report code coverage - uses: zgosalvez/github-actions-report-lcov@v3 + uses: zgosalvez/github-actions-report-lcov@v4.1.22 with: coverage-files: lcov.info artifact-name: code-coverage-report diff --git a/.github/workflows/test-gas.yml b/.github/workflows/test-gas.yml index 4385bb7..47d67a0 100644 --- a/.github/workflows/test-gas.yml +++ b/.github/workflows/test-gas.yml @@ -36,7 +36,7 @@ jobs: run: make gas-report - name: Compare gas reports - uses: Rubilmax/foundry-gas-diff@v3.16 + uses: Rubilmax/foundry-gas-diff@v3.21 id: gas_diff - name: Add gas diff to sticky comment diff --git a/.gitignore b/.gitignore index 3b4340d..dbecd35 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ docs/ # NPM modules node_modules gasreport.ansi + +#soljson +soljson-latest.js diff --git a/.gitmodules b/.gitmodules index cabe015..1bc2050 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,11 @@ [submodule "lib/forge-std"] path = lib/forge-std - url = git@github.com:foundry-rs/forge-std.git + url = https://github.com/foundry-rs/forge-std branch = v1 [submodule "lib/common"] path = lib/common - url = git@github.com:MZero-Labs/common.git + url = https://github.com/m0-foundation/common + branch = release-v1.5.2 +[submodule "lib/evm-m-extensions"] + path = lib/evm-m-extensions + url = https://github.com/m0-foundation/evm-m-extensions diff --git a/.prettierrc b/.prettierrc index 954e185..14d1c71 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,7 +7,7 @@ "files": "*.sol", "options": { "bracketSpacing": true, - "compiler": "0.8.23", + "compiler": "0.8.26", "parser": "solidity-parse", "printWidth": 120, "tabWidth": 4, diff --git a/.solhint.json b/.solhint.json index 3c7c11c..fa3074b 100644 --- a/.solhint.json +++ b/.solhint.json @@ -13,7 +13,7 @@ ], "compiler-version": [ "error", - "0.8.23" + "0.8.26" ], "comprehensive-interface": "off", "const-name-snakecase": "off", diff --git a/Makefile b/Makefile index d155784..805bc44 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,13 @@ update:; forge update # Deployment helpers deploy: - FOUNDRY_PROFILE=production forge script script/DeployProduction.s.sol --skip src --skip test --rpc-url mainnet --slow --broadcast -vvv --verify + FOUNDRY_PROFILE=production forge script script/Deploy.s.sol --skip src --skip test --rpc-url ${DEPLOY_RPC_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --slow --broadcast -vvv --verify --show-standard-json-input deploy-local: - FOUNDRY_PROFILE=production forge script script/DeployProduction.s.sol --skip src --skip test --rpc-url localhost --slow --broadcast -vvv + FOUNDRY_PROFILE=production forge script script/Deploy.s.sol --skip src --skip test --rpc-url localhost --slow --broadcast -vvv + +deploy-upgrade: + FOUNDRY_PROFILE=production forge script script/DeployUpgradeMainnet.s.sol --skip src --skip test --rpc-url mainnet --slow --broadcast -vvv --verify --show-standard-json-input # Run slither slither : @@ -35,10 +38,10 @@ invariant: MAINNET_RPC_URL=$(MAINNET_RPC_URL) ./test.sh -d test/invariant -p $(profile) coverage: - MAINNET_RPC_URL=$(MAINNET_RPC_URL) forge coverage --no-match-path 'test/in*/**/*.sol' --report lcov && lcov --extract lcov.info --rc lcov_branch_coverage=1 --rc derive_function_end_line=0 -o lcov.info 'src/*' && genhtml lcov.info --rc branch_coverage=1 --rc derive_function_end_line=0 -o coverage + FOUNDRY_PROFILE=production forge coverage --fork-url $(MAINNET_RPC_URL) --report lcov && lcov --extract lcov.info --rc lcov_branch_coverage=1 --rc derive_function_end_line=0 -o lcov.info 'src/*' && genhtml lcov.info --rc branch_coverage=1 --rc derive_function_end_line=0 -o coverage gas-report: - FOUNDRY_PROFILE=production forge test --no-match-path 'test/integration/**/*.sol' --gas-report > gasreport.ansi + FOUNDRY_PROFILE=production forge test --fork-url $(MAINNET_RPC_URL) --gas-report > gasreport.ansi sizes: ./build.sh -p production -s diff --git a/README.md b/README.md index 6acc681..c4e4cf9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ You may have to install the following tools to use this repository: - [foundry](https://github.com/foundry-rs/foundry) to compile and test contracts - [lcov](https://github.com/linux-test-project/lcov) to generate the code coverage report -- [yarn](https://classic.yarnpkg.com/lang/en/docs/install/) to manage node dependencies - [slither](https://github.com/crytic/slither) to static analyze contracts Install dependencies: diff --git a/audits/ChainSecurity_M0Labs_WrappedMTokenv2_Audit.pdf b/audits/ChainSecurity_M0Labs_WrappedMTokenv2_Audit.pdf new file mode 100644 index 0000000..7f2b673 Binary files /dev/null and b/audits/ChainSecurity_M0Labs_WrappedMTokenv2_Audit.pdf differ diff --git a/earners/arbitrum.csv b/earners/arbitrum.csv new file mode 100644 index 0000000..629bca0 --- /dev/null +++ b/earners/arbitrum.csv @@ -0,0 +1,4 @@ +address,balance +0x0a1a1a107e45b7ced86833863f482bc5f4ed82ef,678979424224816 +0x0b2b2b2076d95dda7817e785989fe353fe955ef9,6094692055093 +0xb50a1f651a5acb2679c8f679d782c728f3702e53,6039117591036 \ No newline at end of file diff --git a/earners/ethereum.csv b/earners/ethereum.csv new file mode 100644 index 0000000..793d360 --- /dev/null +++ b/earners/ethereum.csv @@ -0,0 +1,25 @@ +address,balance +0x4cbc25559dbbd1272ec5b64c7b5f48a2405e6470,68218629706363 +0xff95c5f35f4ffb9d5f596f898ac1ae38d62749c2,14498026276966 +0x0502d65f26f45d17503e4d34441f5e73ea143033,2576885412528 +0x970a7749ecaa4394c8b2bf5f2471f41fd6b79288,2335445266321 +0x81ad394c0fa87e99ca46e1aca093bee020f203f4,73701877312 +0xfe940bfe535013a52e8e2df9644f95e3c94fa14b,23142486046 +0xe0663f2372caa1459b7ade90812dc737ce587fa6,6847088454 +0xa969cfcd9e583edb8c8b270dc8cafb33d6cf662d,3080743473 +0x9c6e67fa86138ab49359f595bfe4fb163d0f16cc,2536640986 +0xdd82875f0840aad58a455a70b88eed9f59cec7c7,1702276055 +0xb65a66621d7de34afec9b9ac0755133051550dd7,891681763 +0xcad001c30e96765ac90307669d578219d4fb1dce,85322234 +0xded796de6a14e255487191963dee436c45995813,66920585 +0xea0c048c728578b1510ebdf9b692e8936d6fbc90,34057216 +0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb,7137461 +0x13ccb6e28f22e2f6783badedce32cc74583a3647,337592 +0x985de23260743c2c2f09bfdec50b048c7a18c461,287386 +0x7db685961f97c847a4c815d43e9cc0e5647328b9,41793 +0x48afe17cb6363fd1aaea50a8cb652c5978972c96,1931 +0xe72fe64840f4ef80e3ec73a1c749491b5c938cb9,988 +0xcf3166181848eec4fd3b9046ae7cb582f34d2e6c,0 +0xb50a1f651a5acb2679c8f679d782c728f3702e53,0 +0x9f6d1a62bf268aa05a1218cfc89c69833d2d2a70,0 +0x569d7dccbf6923350521ecbc28a555a500c4f0ec,0 \ No newline at end of file diff --git a/earners/get-earners.ts b/earners/get-earners.ts new file mode 100755 index 0000000..7846444 --- /dev/null +++ b/earners/get-earners.ts @@ -0,0 +1,130 @@ +#!/usr/bin/env tsx + +import { writeFileSync, mkdirSync } from "fs"; +import { join } from "path"; + +interface WMHolder { + address: string; + balance: string; + isEarning: boolean; +} + +interface GraphQLResponse { + data?: { + WMHolders?: WMHolder[]; + WMHoldersArbitrum?: WMHolder[]; + }; + errors?: Array<{ message: string }>; +} + +const PROTOCOL_API_URL = "https://protocol-api.m0.org/graphql"; + +async function fetchWMHolders(network: string): Promise { + const fieldName = network === "ethereum" ? "WMHolders" : "WMHoldersL2"; + const chainArg = + network === "ethereum" ? "" : `chain: ${network.toUpperCase()}`; + + const query = ` + query GetWMHolders { + ${fieldName}(first: 1000, ${chainArg}) { + address + balance + isEarning + } + } + `; + + const response = await fetch(PROTOCOL_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ query }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result: GraphQLResponse = await response.json(); + + if (result.errors) { + throw new Error( + `GraphQL errors: ${result.errors.map((e) => e.message).join(", ")}`, + ); + } + + return result.data?.[fieldName as keyof GraphQLResponse["data"]] || []; +} + +function convertToCSV(holders: WMHolder[]): string { + const headers = ["address", "balance"]; + const rows = holders.map((holder) => [holder.address, holder.balance]); + + return [headers, ...rows].map((row) => row.join(",")).join("\n"); +} + +async function main() { + const networks = [ + "arbitrum", + "base", + "bsc", + "ethereum", + "hyperevm", + "linea", + "mantra", + "optimism", + "plasma", + "plume", + "soneium", + ]; + + mkdirSync("earners", { recursive: true }); + + for (const network of networks) { + const networkName = network.charAt(0).toUpperCase() + network.slice(1); + + try { + console.log(`\nFetching WrappedM holders from ${networkName}...`); + + const holders = await fetchWMHolders(network); + console.log(`Found ${holders.length} WrappedM holders on ${networkName}`); + + if (holders.length === 0) { + console.log("No holders found"); + continue; + } + + const earners = holders.filter((holder) => holder.isEarning); + console.log( + `Found ${earners.length} WrappedM earners (isEarning = true)`, + ); + + if (earners.length === 0) { + console.log("No earners found"); + continue; + } + + const csvPath = join("earners", `${network}.csv`); + const csvContent = convertToCSV(earners); + + writeFileSync(csvPath, csvContent); + console.log(`Exported ${earners.length} earners to ${csvPath}`); + + const totalBalance = earners.reduce( + (sum, h) => sum + parseFloat(h.balance || "0"), + 0, + ); + + console.log(`Total balance: ${totalBalance}`); + console.log(`Total earners: ${earners.length}`); + } catch (error) { + console.error( + `Error processing ${networkName}:`, + error instanceof Error ? error.message : error, + ); + } + } +} + +main(); diff --git a/earners/plume.csv b/earners/plume.csv new file mode 100644 index 0000000..7be1e8a --- /dev/null +++ b/earners/plume.csv @@ -0,0 +1,2 @@ +address,balance +0xe72fe64840f4ef80e3ec73a1c749491b5c938cb9,749816515 \ No newline at end of file diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..65f2d0a --- /dev/null +++ b/foundry.lock @@ -0,0 +1,17 @@ +{ + "lib/common": { + "branch": { + "name": "release-v1.5.2", + "rev": "489fae6daecd227616155e4f8cacfd17aeb3e2a1" + } + }, + "lib/evm-m-extensions": { + "rev": "87a2f42ab55b5e10771f48fa0d5994bfaa3dca09" + }, + "lib/forge-std": { + "branch": { + "name": "v1", + "rev": "620536fa5277db4e3fd46772d5cbc1ea0696fb43" + } + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 016467e..5ea4429 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,15 +2,16 @@ gas_reports = ["*"] gas_reports_ignore = [] ignored_error_codes = [] -solc_version = "0.8.23" +solc_version = "0.8.26" optimizer = true -optimizer_runs = 999999 +optimizer_runs = 4150 verbosity = 3 -block_number = 20_270_778 -block_timestamp = 1_720_550_400 -fork_block_number = 20_270_778 +fork_block_number = 23_170_985 rpc_storage_caching = { chains = ["mainnet"], endpoints = "all" } -evm_version = "shanghai" +evm_version = "cancun" + +[lint] +lint_on_build = false [profile.production] build_info = true diff --git a/lib/common b/lib/common index 45aa01d..489fae6 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 45aa01db3acf6189e6b26ada5cd5ac8351837b56 +Subproject commit 489fae6daecd227616155e4f8cacfd17aeb3e2a1 diff --git a/lib/evm-m-extensions b/lib/evm-m-extensions new file mode 160000 index 0000000..87a2f42 --- /dev/null +++ b/lib/evm-m-extensions @@ -0,0 +1 @@ +Subproject commit 87a2f42ab55b5e10771f48fa0d5994bfaa3dca09 diff --git a/lib/forge-std b/lib/forge-std index bf66061..620536f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 +Subproject commit 620536fa5277db4e3fd46772d5cbc1ea0696fb43 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..36888b3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2200 @@ +{ + "name": "@mzero-labs/wrapped-m-token", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@mzero-labs/wrapped-m-token", + "version": "2.0.0", + "devDependencies": { + "ethers": "^6.16.0", + "lint-staged": "^15.2.2", + "prettier": "^3.3.2", + "prettier-plugin-solidity": "^1.3.1", + "solhint": "^4.5.2", + "solhint-plugin-prettier": "^0.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "dev": true, + "license": "MIT" + }, + "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.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/@prettier/sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@prettier/sync/-/sync-0.3.0.tgz", + "integrity": "sha512-3dcmCyAxIcxy036h1I7MQU/uEEBq8oLwf1CE3xeze+MPlgkdlb/+w6rGR/1dhp6Hqi17fRS6nvwnOzkESxEkOw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/prettier/prettier-synchronized?sponsor=1" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, + "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/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true, + "license": "MIT" + }, + "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", + "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/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "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/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/cacheable-request/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/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": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?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": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "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": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "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": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "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/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "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": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "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/got/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/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/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "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": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "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/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "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.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "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/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "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": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.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": ">=18.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/log-update/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/log-update/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/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/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "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-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-solidity": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.4.3.tgz", + "integrity": "sha512-Mrr/iiR9f9IaeGRMZY2ApumXcn/C5Gs3S7B7hWB3gigBFML06C0yEyW86oLp0eqiA0qg+46FaChgLPJCj/pIlg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@solidity-parser/parser": "^0.20.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "prettier": ">=2.3.0" + } + }, + "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/restore-cursor/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/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.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/solhint": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-4.5.4.tgz", + "integrity": "sha512-Cu1XiJXub2q1eCr9kkJ9VPv1sGcmj3V7Zb76B0CoezDOB9bu3DxKIFFH7ggCl9fWpEPD6xBmRLfZrYijkVmujQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@solidity-parser/parser": "^0.18.0", + "ajv": "^6.12.6", + "antlr4": "^4.13.1-patch-1", + "ast-parents": "^0.0.1", + "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", + "strip-ansi": "^6.0.1", + "table": "^6.8.1", + "text-table": "^0.2.0" + }, + "bin": { + "solhint": "solhint.js" + }, + "optionalDependencies": { + "prettier": "^2.8.3" + } + }, + "node_modules/solhint-plugin-prettier": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/solhint-plugin-prettier/-/solhint-plugin-prettier-0.1.0.tgz", + "integrity": "sha512-SDOTSM6tZxZ6hamrzl3GUgzF77FM6jZplgL2plFBclj/OjKP8Z3eIPojKU73gRr0MvOS8ACZILn8a5g0VTz/Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@prettier/sync": "^0.3.0", + "prettier-linter-helpers": "^1.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-solidity": "^1.0.0" + } + }, + "node_modules/solhint/node_modules/@solidity-parser/parser": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.18.0.tgz", + "integrity": "sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/solhint/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/solhint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/solhint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/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/solhint/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/solhint/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/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": "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/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-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/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/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/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/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json index 2082010..3cb2bc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mzero-labs/wrapped-m-token", - "version": "1.0.0", + "version": "2.0.0", "description": "Wrapped M Token", "author": "M^0 Labs ", "repository": { @@ -31,6 +31,7 @@ "test-invariant": "make -B invariant" }, "devDependencies": { + "ethers": "^6.16.0", "lint-staged": "^15.2.2", "prettier": "^3.3.2", "prettier-plugin-solidity": "^1.3.1", diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..9f36445 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { Script, console2 } from "../lib/forge-std/src/Script.sol"; + +import { DeployBase } from "./DeployBase.sol"; + +contract DeployProduction is Script, DeployBase { + error DeployerMismatch(address expected, address actual); + + error DeployerNonceTooHigh(); + + error UnexpectedDeployerNonce(); + + error CurrentNonceMismatch(uint64 expected, uint64 actual); + + error ExpectedProxyMismatch(address expected, address actual); + + error ResultingProxyMismatch(address expected, address actual); + + function run() external { + address deployer_ = vm.rememberKey(vm.envUint("PRIVATE_KEY")); + address expectedDeployer_ = vm.envAddress("DEPLOYER"); + + uint64 deployerWrappedMProxyNonce_ = uint64(vm.envUint("DEPLOYER_WRAPPED_M_PROXY_NONCE")); + + address expectedWrappedMProxy_ = vm.envAddress("EXPECTED_WRAPPED_M_PROXY"); + + console2.log("Deployer:", deployer_); + + if (deployer_ != expectedDeployer_) revert DeployerMismatch(expectedDeployer_, deployer_); + + uint64 currentNonce_ = vm.getNonce(deployer_); + + uint64 startNonce_ = currentNonce_; + address wrappedMTokenImplementation_; + address wrappedMTokenProxy_; + + while (true) { + if (startNonce_ > deployerWrappedMProxyNonce_) revert DeployerNonceTooHigh(); + + (wrappedMTokenImplementation_, wrappedMTokenProxy_) = mockDeploy(deployer_, startNonce_); + + if (wrappedMTokenProxy_ == expectedWrappedMProxy_) break; + + ++startNonce_; + } + + vm.startBroadcast(deployer_); + + // Burn nonces until to `currentNonce_ == startNonce_`. + while (currentNonce_ < startNonce_) { + payable(deployer_).transfer(0); + ++currentNonce_; + } + + if (currentNonce_ != vm.getNonce(deployer_)) revert CurrentNonceMismatch(currentNonce_, vm.getNonce(deployer_)); + + if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); + + (wrappedMTokenImplementation_, wrappedMTokenProxy_) = deploy( + vm.envAddress("M_TOKEN"), + vm.envAddress("REGISTRAR"), + vm.envAddress("SWAP_FACILITY"), + vm.envAddress("WRAPPED_M_MIGRATION_ADMIN") + ); + + vm.stopBroadcast(); + + console2.log("Wrapped M Implementation address:", wrappedMTokenImplementation_); + console2.log("Wrapped M Proxy address:", wrappedMTokenProxy_); + + if (wrappedMTokenProxy_ != expectedWrappedMProxy_) + revert ResultingProxyMismatch(expectedWrappedMProxy_, wrappedMTokenProxy_); + } +} diff --git a/script/DeployBase.sol b/script/DeployBase.sol index 857b601..61494a2 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -1,57 +1,147 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { ContractHelper } from "../lib/common/src/libs/ContractHelper.sol"; +import { Proxy } from "../lib/common/src/Proxy.sol"; +import { WrappedMTokenMigratorV1 } from "../src/WrappedMTokenMigratorV1.sol"; import { WrappedMToken } from "../src/WrappedMToken.sol"; -import { Proxy } from "../src/Proxy.sol"; contract DeployBase { + /** + * @dev Groups the Wrapped M governance role addresses passed to the Migrator's `initialize`. + * @param admin The address of the Wrapped M admin. + * @param freezeManager The address of the Wrapped M freeze manager. + * @param pauser The address of the Wrapped M pauser. + * @param forcedTransferManager The address of the Wrapped M forced transfer manager. + * @param excessManager The address of the Wrapped M excess manager. + */ + struct UpgradeRoles { + address admin; + address freezeManager; + address pauser; + address forcedTransferManager; + address excessManager; + } + /** * @dev Deploys Wrapped M Token. - * @param mToken_ The address the M Token contract. - * @param migrationAdmin_ The address the Migration Admin. - * @return implementation_ The address of the deployed Wrapped M Token implementation. - * @return proxy_ The address of the deployed Wrapped M Token proxy. + * @param mToken_ The address of the M Token contract. + * @param registrar_ The address of the Registrar contract. + * @param swapFacility_ The address of the SwapFacility contract. + * @param wrappedMMigrationAdmin_ The address of the Wrapped M Migration Admin. + * @return wrappedMTokenImplementation_ The address of the deployed Wrapped M Token implementation. + * @return wrappedMTokenProxy_ The address of the deployed Wrapped M Token proxy. */ function deploy( address mToken_, - address migrationAdmin_ - ) public virtual returns (address implementation_, address proxy_) { - // Wrapped M token needs `mToken_` and `migrationAdmin_` addresses. - // Proxy needs `implementation_` addresses. + address registrar_, + address swapFacility_, + address wrappedMMigrationAdmin_ + ) public virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenProxy_) { + wrappedMTokenImplementation_ = address( + new WrappedMToken(mToken_, registrar_, swapFacility_, wrappedMMigrationAdmin_) + ); - implementation_ = address(new WrappedMToken(mToken_, migrationAdmin_)); - proxy_ = address(new Proxy(implementation_)); + wrappedMTokenProxy_ = address(new Proxy(wrappedMTokenImplementation_)); } - function _getExpectedWrappedMTokenImplementation( - address deployer_, - uint256 deployerNonce_ - ) internal pure returns (address) { - return ContractHelper.getContractFrom(deployer_, deployerNonce_); + /** + * @dev Deploys Wrapped M Token components needed to upgrade an existing Wrapped M proxy. + * @param mToken_ The address of the M Token contract. + * @param registrar_ The address of the Registrar contract. + * @param excessDestination_ The address of the excess destination. + * @param swapFacility_ The address of the SwapFacility contract. + * @param wrappedMMigrationAdmin_ The address of the Wrapped M Migration Admin. + * @param earners_ The addresses of the earners to migrate. + * @param roles_ The Wrapped M governance role addresses. + * @return wrappedMTokenImplementation_ The address of the deployed Wrapped M Token implementation. + * @return wrappedMTokenMigrator_ The address of the deployed Wrapped M Token Migrator. + */ + function deployUpgrade( + address mToken_, + address registrar_, + address excessDestination_, + address swapFacility_, + address wrappedMMigrationAdmin_, + address[] memory earners_, + UpgradeRoles memory roles_ + ) public virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) { + wrappedMTokenImplementation_ = address( + new WrappedMToken(mToken_, registrar_, swapFacility_, wrappedMMigrationAdmin_) + ); + + wrappedMTokenMigrator_ = _deployMigrator(wrappedMTokenImplementation_, earners_, excessDestination_, roles_); } - function getExpectedWrappedMTokenImplementation( - address deployer_, - uint256 deployerNonce_ - ) public pure virtual returns (address) { - return _getExpectedWrappedMTokenImplementation(deployer_, deployerNonce_); + /// @dev Deploys the Wrapped M Token Migrator (split out to avoid stack-too-deep in `deployUpgrade`). + function _deployMigrator( + address implementation_, + address[] memory earners_, + address excessDestination_, + UpgradeRoles memory roles_ + ) internal returns (address migrator_) { + earners_ = _sortAddresses(earners_); + + return + address( + new WrappedMTokenMigratorV1( + implementation_, + earners_, + roles_.admin, + roles_.freezeManager, + roles_.pauser, + roles_.forcedTransferManager, + roles_.excessManager, + excessDestination_ + ) + ); } - function _getExpectedWrappedMTokenProxy(address deployer_, uint256 deployerNonce_) internal pure returns (address) { - return ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); + function _sortAddresses(address[] memory addresses_) internal pure returns (address[] memory) { + for (uint256 i_ = 1; i_ < addresses_.length; ++i_) { + address key_ = addresses_[i_]; + uint256 j_ = i_; + + while (j_ > 0 && uint160(addresses_[j_ - 1]) > uint160(key_)) { + addresses_[j_] = addresses_[j_ - 1]; + --j_; + } + + addresses_[j_] = key_; + } + + return addresses_; } - function getExpectedWrappedMTokenProxy( + /** + * @dev Mock deploys Wrapped M Token, returning the would-be addresses. + * @param deployer_ The address of the deployer. + * @param deployerNonce_ The nonce of the deployer. + * @return wrappedMTokenImplementation_ The address of the would-be Wrapped M Token implementation. + * @return wrappedMTokenProxy_ The address of the would-be Wrapped M Token proxy. + */ + function mockDeploy( address deployer_, uint256 deployerNonce_ - ) public pure virtual returns (address) { - return _getExpectedWrappedMTokenProxy(deployer_, deployerNonce_); + ) public view virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenProxy_) { + wrappedMTokenImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + wrappedMTokenProxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); } - function getDeployerNonceAfterProtocolDeployment(uint256 deployerNonce_) public pure virtual returns (uint256) { - return deployerNonce_ + 2; + /** + * @dev Mock deploys Wrapped M Token, returning the would-be addresses. + * @param deployer_ The address of the deployer. + * @param deployerNonce_ The nonce of the deployer. + * @return wrappedMTokenImplementation_ The address of the would-be Wrapped M Token implementation. + * @return wrappedMTokenMigrator_ The address of the deployed Wrapped M Token Migrator. + */ + function mockDeployUpgrade( + address deployer_, + uint256 deployerNonce_ + ) public view virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) { + wrappedMTokenImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + wrappedMTokenMigrator_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); } } diff --git a/script/DeployProduction.s.sol b/script/DeployProduction.s.sol deleted file mode 100644 index 1e9ae7e..0000000 --- a/script/DeployProduction.s.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity 0.8.23; - -import { Script, console2 } from "../lib/forge-std/src/Script.sol"; - -import { DeployBase } from "./DeployBase.sol"; - -contract DeployProduction is Script, DeployBase { - error DeployerMismatch(address expected, address actual); - - error DeployerNonceTooHigh(); - - error UnexpectedDeployerNonce(); - - error CurrentNonceMismatch(uint64 expected, uint64 actual); - - error ExpectedProxyMismatch(address expected, address actual); - - error ResultingProxyMismatch(address expected, address actual); - - // NOTE: Ensure this is the correct M Token testnet/mainnet address. - address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; - - // NOTE: Ensure this is the correct Migration Admin testnet/mainnet address. - address internal constant _MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; - - // NOTE: Ensure this is the correct deployer testnet/mainnet to use. - address internal constant _EXPECTED_DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; - - // NOTE: Ensure this is the correct nonce to use to deploy the Proxy on testnet/mainnet. - uint64 internal constant _DEPLOYER_PROXY_NONCE = 40; - - // NOTE: Ensure this is the correct expected testnet/mainnet address for the Proxy. - address internal constant _EXPECTED_PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; - - function run() external { - address deployer_ = vm.rememberKey(vm.envUint("PRIVATE_KEY")); - - console2.log("Deployer:", deployer_); - - if (deployer_ != _EXPECTED_DEPLOYER) revert DeployerMismatch(_EXPECTED_DEPLOYER, deployer_); - - uint64 currentNonce_ = vm.getNonce(deployer_); - uint64 startNonce_ = _DEPLOYER_PROXY_NONCE - 1; - - if (currentNonce_ >= startNonce_) revert DeployerNonceTooHigh(); - - address expectedProxy_ = getExpectedWrappedMTokenProxy(deployer_, startNonce_); - - if (expectedProxy_ != _EXPECTED_PROXY) revert ExpectedProxyMismatch(_EXPECTED_PROXY, expectedProxy_); - - vm.startBroadcast(deployer_); - - // Burn nonces until to 1 before `_DEPLOYER_PROXY_NONCE` since implementation is deployed before proxy. - while (currentNonce_ < startNonce_) { - payable(deployer_).transfer(0); - ++currentNonce_; - } - - if (currentNonce_ != vm.getNonce(deployer_)) revert CurrentNonceMismatch(currentNonce_, vm.getNonce(deployer_)); - - if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); - - (address implementation_, address proxy_) = deploy(_M_TOKEN, _MIGRATION_ADMIN); - - vm.stopBroadcast(); - - console2.log("Wrapped M Implementation address:", implementation_); - console2.log("Wrapped M Proxy address:", proxy_); - - if (proxy_ != _EXPECTED_PROXY) revert ResultingProxyMismatch(_EXPECTED_PROXY, proxy_); - } -} diff --git a/script/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol new file mode 100644 index 0000000..8c76a34 --- /dev/null +++ b/script/DeployUpgradeMainnet.s.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { Script, console2 } from "../lib/forge-std/src/Script.sol"; + +import { DeployBase } from "./DeployBase.sol"; +import { EarnersAddresses } from "./EarnersAddresses.sol"; + +contract DeployUpgradeMainnet is Script, DeployBase { + error DeployerMismatch(address expected, address actual); + + error DeployerNonceTooHigh(); + + error UnexpectedDeployerNonce(); + + error CurrentNonceMismatch(uint64 expected, uint64 actual); + + error ResultingMigratorMismatch(address expected, address actual); + + address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; + + // NOTE: Ensure this is the correct Excess Destination mainnet address. + address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault + + address internal constant _SWAP_FACILITY = 0xB6807116b3B1B321a390594e31ECD6e0076f6278; + + address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; + + // NOTE: Ensure this is the correct Migration Admin mainnet address. + address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + + address internal constant _WRAPPED_M_PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; // Mainnet address for the Proxy. + + // NOTE: Ensure this is the correct admin to use. + address internal constant _ADMIN = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + // NOTE: Ensure this is the correct freeze manager to use. + address internal constant _FREEZE_MANAGER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + // NOTE: Ensure this is the correct pauser to use. + address internal constant _PAUSER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + // NOTE: Ensure this is the correct forced transfer manager to use. + address internal constant _FORCED_TRANSFER_MANAGER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + // NOTE: Ensure this is the correct excess manager to use. + address internal constant _EXCESS_MANAGER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + // NOTE: Ensure this is the correct mainnet deployer to use. + address internal constant _EXPECTED_DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + // NOTE: Ensure this is the correct nonce to use to deploy the Migrator on mainnet. + uint64 internal constant _DEPLOYER_MIGRATOR_NONCE = 40; + + // NOTE: Ensure this is the correct expected mainnet address for the Migrator. + address internal constant _EXPECTED_WRAPPED_M_MIGRATOR = address(0); + + // NOTE: Earners are loaded dynamically from EarnersAddresses.sol library + function getEarners() internal view returns (address[] memory) { + if (block.chainid == 1) { + return EarnersAddresses.getEthereumEarners(); + } else if (block.chainid == 42161) { + return EarnersAddresses.getArbitrumEarners(); + } else if (block.chainid == 98866) { + return EarnersAddresses.getPlumeEarners(); + } else { + revert("Unsupported chain ID"); + } + } + + function run() external { + address deployer_ = vm.rememberKey(vm.envUint("PRIVATE_KEY")); + + console2.log("Deployer:", deployer_); + + if (deployer_ != _EXPECTED_DEPLOYER) revert DeployerMismatch(_EXPECTED_DEPLOYER, deployer_); + + uint64 currentNonce_ = vm.getNonce(deployer_); + + uint64 startNonce_ = currentNonce_; + address wrappedMTokenImplementation_; + address wrappedMTokenMigrator_; + + while (true) { + if (startNonce_ > _DEPLOYER_MIGRATOR_NONCE) revert DeployerNonceTooHigh(); + + (wrappedMTokenImplementation_, wrappedMTokenMigrator_) = mockDeployUpgrade(deployer_, startNonce_); + + if (wrappedMTokenMigrator_ == _EXPECTED_WRAPPED_M_MIGRATOR) break; + + ++startNonce_; + } + + vm.startBroadcast(deployer_); + + // Burn nonces until to `currentNonce_ == startNonce_`. + while (currentNonce_ < startNonce_) { + payable(deployer_).transfer(0); + ++currentNonce_; + } + + if (currentNonce_ != vm.getNonce(deployer_)) revert CurrentNonceMismatch(currentNonce_, vm.getNonce(deployer_)); + + if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); + + address[] memory earners_ = getEarners(); + + (wrappedMTokenImplementation_, wrappedMTokenMigrator_) = deployUpgrade( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _SWAP_FACILITY, + _WRAPPED_M_MIGRATION_ADMIN, + earners_, + UpgradeRoles({ + admin: _ADMIN, + freezeManager: _FREEZE_MANAGER, + pauser: _PAUSER, + forcedTransferManager: _FORCED_TRANSFER_MANAGER, + excessManager: _EXCESS_MANAGER + }) + ); + + vm.stopBroadcast(); + + console2.log("Wrapped M Implementation address:", wrappedMTokenImplementation_); + console2.log("Migrator address:", wrappedMTokenMigrator_); + + if (wrappedMTokenMigrator_ != _EXPECTED_WRAPPED_M_MIGRATOR) { + revert ResultingMigratorMismatch(_EXPECTED_WRAPPED_M_MIGRATOR, wrappedMTokenMigrator_); + } + } +} diff --git a/script/EarnersAddresses.sol b/script/EarnersAddresses.sol new file mode 100644 index 0000000..2d0cb9a --- /dev/null +++ b/script/EarnersAddresses.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.26; + +library EarnersAddresses { + function getArbitrumEarners() internal pure returns (address[] memory) { + address[3] memory earners = [ + 0x0A1a1A107E45b7Ced86833863f482BC5f4ed82EF, + 0x0B2b2B2076d95dda7817e785989fE353fe955ef9, + 0xB50A1f651A5ACb2679c8f679D782c728f3702E53 + ]; + address[] memory result = new address[](3); + for (uint256 i = 0; i < 3; ++i) { + result[i] = earners[i]; + } + return result; + } + function getEthereumEarners() internal pure returns (address[] memory) { + address[24] memory earners = [ + 0x0502d65f26f45d17503E4d34441F5e73Ea143033, + 0x13Ccb6E28F22E2f6783BaDedCe32cc74583A3647, + 0x48Afe17cB6363fD1aaeA50a8CB652C5978972c96, + 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, + 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec, + 0x7db685961F97c847A4C815D43E9cc0E5647328b9, + 0x81ad394C0Fa87e99Ca46E1aca093BEe020f203f4, + 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, + 0x985DE23260743c2c2f09BFdeC50b048C7a18c461, + 0x9c6e67fA86138Ab49359F595BfE4Fb163D0f16cc, + 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70, + 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, + 0xB50A1f651A5ACb2679c8f679D782c728f3702E53, + 0xB65a66621D7dE34afec9b9AC0755133051550dD7, + 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, + 0xcAD001c30E96765aC90307669d578219D4fb1DCe, + 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, + 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, + 0xDeD796De6a14E255487191963dEe436c45995813, + 0xE0663f2372cAa1459b7ade90812Dc737CE587FA6, + 0xE72Fe64840F4EF80E3Ec73a1c749491b5c938CB9, + 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90, + 0xfE940BFE535013a52e8e2DF9644f95E3C94fa14B, + 0xfF95c5f35F4ffB9d5f596F898ac1ae38D62749c2 + ]; + address[] memory result = new address[](24); + for (uint256 i = 0; i < 24; ++i) { + result[i] = earners[i]; + } + return result; + } + function getPlumeEarners() internal pure returns (address[] memory) { + address[1] memory earners = [0xE72Fe64840F4EF80E3Ec73a1c749491b5c938CB9]; + address[] memory result = new address[](1); + for (uint256 i = 0; i < 1; ++i) { + result[i] = earners[i]; + } + return result; + } +} diff --git a/script/generate-earners-array.ts b/script/generate-earners-array.ts new file mode 100755 index 0000000..24f0977 --- /dev/null +++ b/script/generate-earners-array.ts @@ -0,0 +1,215 @@ +#!/usr/bin/env tsx + +import { writeFileSync } from "fs"; +import { join } from "path"; +import { getAddress } from "ethers"; + +interface WMHolder { + address: string; + isEarning: boolean; +} + +interface GraphQLResponse { + data?: Record; + errors?: Array<{ message: string }>; +} + +const PROTOCOL_API_URL = "https://protocol-api.m0.org/graphql"; +const OUTPUT_PATH = join("script", "EarnersAddresses.sol"); +const PAGE_SIZE = 1000; +const MAX_PAGES = 100; +const REQUEST_TIMEOUT_MS = 30_000; + +const NETWORKS = [ + "arbitrum", + "base", + "bsc", + "ethereum", + "hyperevm", + "linea", + "mantra", + "optimism", + "plasma", + "plume", + "soneium", +]; + +// NOTE: The `chain` argument must match the GraphQL `Chain` enum exactly. It defaults to the +// uppercased network name; add an entry here if a network's enum value differs. +const CHAIN_ENUM_OVERRIDES: Record = {}; + +function chainEnumFor(network: string): string { + return CHAIN_ENUM_OVERRIDES[network] ?? network.toUpperCase(); +} + +function fieldNameFor(network: string): string { + return network === "ethereum" ? "WMHolders" : "WMHoldersL2"; +} + +function generateFunctionName(network: string): string { + return `get${network.charAt(0).toUpperCase() + network.slice(1)}Earners`; +} + +async function graphqlRequest(query: string): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + + try { + const response = await fetch(PROTOCOL_API_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query }), + signal: controller.signal, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result: GraphQLResponse = await response.json(); + + if (result.errors) { + throw new Error( + `GraphQL errors: ${result.errors.map((e) => e.message).join(", ")}`, + ); + } + + return result; + } finally { + clearTimeout(timeout); + } +} + +async function fetchWMHolders(network: string): Promise { + const fieldName = fieldNameFor(network); + const chainArg = + network === "ethereum" ? "" : `, chain: ${chainEnumFor(network)}`; + const holders: WMHolder[] = []; + + // NOTE: Paginate until a short page is returned so that earners are never silently truncated by + // the `first` cap. The `MAX_PAGES` guard prevents an infinite loop if `skip` is ignored. + for (let page = 0; page < MAX_PAGES; ++page) { + const query = ` + query GetWMHolders { + ${fieldName}(first: ${PAGE_SIZE}, skip: ${page * PAGE_SIZE}${chainArg}) { + address + isEarning + } + } + `; + + const result = await graphqlRequest(query); + const pageHolders = result.data?.[fieldName] ?? []; + + holders.push(...pageHolders); + + if (pageHolders.length < PAGE_SIZE) return holders; + } + + throw new Error( + `Exceeded ${MAX_PAGES} pages (${MAX_PAGES * PAGE_SIZE} holders) fetching ${network}; pagination may be broken`, + ); +} + +function toSortedUniqueAddresses( + earners: WMHolder[], + network: string, +): string[] { + const sorted = earners + .map((e) => { + const address = getAddress(e.address); + return { address, value: BigInt(address) }; + }) + .sort((a, b) => (a.value < b.value ? -1 : a.value > b.value ? 1 : 0)); + + for (let i = 0; i < sorted.length; ++i) { + if (sorted[i].value === 0n) { + throw new Error(`Zero address present in ${network} earners`); + } + + if (i > 0 && sorted[i].value === sorted[i - 1].value) { + throw new Error( + `Duplicate earner address ${sorted[i].address} on ${network}`, + ); + } + } + + return sorted.map((e) => e.address); +} + +function generateSolidityFunction( + network: string, + addresses: string[], +): string { + const functionName = generateFunctionName(network); + const arrayLength = addresses.length; + + const addressesLines = addresses + .map((addr) => ` ${addr}`) + .join(",\n"); + + return ` function ${functionName}() internal pure returns (address[] memory) { + address[${arrayLength}] memory earners = [ +${addressesLines} + ]; + address[] memory result = new address[](${arrayLength}); + for (uint256 i = 0; i < ${arrayLength}; ++i) { + result[i] = earners[i]; + } + return result; + } +`; +} + +function buildSolidityFile(functions: string[]): string { + return `// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.26; + +library EarnersAddresses { +${functions.join("")}} +`; +} + +async function main(): Promise { + // NOTE: Build the whole file in memory and only write once every network succeeds. A failed or + // partial run must never leave a file mixing fresh and stale earner lists. + const functions: string[] = []; + + for (const network of NETWORKS) { + const networkName = network.charAt(0).toUpperCase() + network.slice(1); + + console.log(`\nFetching WrappedM holders from ${networkName}...`); + + const holders = await fetchWMHolders(network); + console.log(`Found ${holders.length} WrappedM holders on ${networkName}`); + + const earners = holders.filter((holder) => holder.isEarning); + console.log(`Found ${earners.length} WrappedM earners (isEarning = true)`); + + if (earners.length === 0) { + console.log("No earners found, skipping"); + continue; + } + + const addresses = toSortedUniqueAddresses(earners, network); + functions.push(generateSolidityFunction(network, addresses)); + + console.log( + `Generated ${generateFunctionName(network)}() with ${addresses.length} checksummed addresses`, + ); + } + + writeFileSync(OUTPUT_PATH, buildSolidityFile(functions)); + console.log( + `\nWrote ${functions.length} earner function(s) to ${OUTPUT_PATH}`, + ); +} + +main().catch((error) => { + console.error( + "Failed to generate earners list:", + error instanceof Error ? error.message : error, + ); + process.exitCode = 1; +}); diff --git a/src/ListOfEarnersToMigrate.sol b/src/ListOfEarnersToMigrate.sol new file mode 100644 index 0000000..c0b3572 --- /dev/null +++ b/src/ListOfEarnersToMigrate.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.26; + +/** + * @title Helper contract to retrieve earners for migrating a WrappedMToken contract from V1 to V2. + * @author M0 Labs + */ +contract ListOfEarnersToMigrate { + /// @notice Emitted when the earners array is not strictly ascending (which would allow duplicates or the zero address). + error EarnersNotSortedOrUnique(); + + address[] public earners; + + constructor(address[] memory earners_) { + // NOTE: Requiring strictly ascending addresses guarantees the list is sorted, free of duplicates and free of + // the zero address. Duplicates would corrupt the v1->v2 principal migration, as a repeated address + // would have its `lastIndex` slot overwritten by the principal computed on the first pass. + uint160 previous_; + + for (uint256 i; i < earners_.length; ++i) { + uint160 current_ = uint160(earners_[i]); + + if (current_ <= previous_) revert EarnersNotSortedOrUnique(); + + previous_ = current_; + } + + earners = earners_; + } + + function getEarners() external view returns (address[] memory earners_) { + return earners; + } +} diff --git a/src/Migratable.sol b/src/Migratable.sol deleted file mode 100644 index 055f51f..0000000 --- a/src/Migratable.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.23; - -import { IMigratable } from "./interfaces/IMigratable.sol"; - -/** - * @title Abstract implementation for exposing the ability to migrate a contract, extending ERC-1967. - * @author M^0 Labs - */ -abstract contract Migratable is IMigratable { - /* ============ Variables ============ */ - - /// @dev Storage slot with the address of the current factory. `keccak256('eip1967.proxy.implementation') - 1`. - uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /* ============ Interactive Functions ============ */ - - /// @inheritdoc IMigratable - function migrate() external { - _migrate(_getMigrator()); - } - - /* ============ View/Pure Functions ============ */ - - /// @inheritdoc IMigratable - function implementation() public view returns (address implementation_) { - assembly { - implementation_ := sload(_IMPLEMENTATION_SLOT) - } - } - - /* ============ Internal Interactive Functions ============ */ - - /** - * @dev Performs an arbitrary migration by delegate-calling `migrator_`. - * @param migrator_ The address of a migrator contract. - */ - function _migrate(address migrator_) internal { - if (migrator_ == address(0)) revert ZeroMigrator(); - - if (migrator_.code.length == 0) revert InvalidMigrator(); - - address oldImplementation_ = implementation(); - - (bool success_, ) = migrator_.delegatecall(""); - if (!success_) revert MigrationFailed(); - - address newImplementation_ = implementation(); - - emit Migrated(migrator_, oldImplementation_, newImplementation_); - - // NOTE: Redundant event emitted to conform to the EIP-1967 standard. - emit Upgraded(newImplementation_); - } - - /* ============ Internal View/Pure Functions ============ */ - - /// @dev Returns the address of a migrator contract. - function _getMigrator() internal view virtual returns (address migrator_); -} diff --git a/src/Proxy.sol b/src/Proxy.sol deleted file mode 100644 index 2cd0024..0000000 --- a/src/Proxy.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.23; -/** - * @title Minimal transparent proxy. - * @author M^0 Labs - */ -contract Proxy { - /// @dev Storage slot with the address of the current factory. `keccak256('eip1967.proxy.implementation') - 1`. - uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /** - * @dev Constructs the contract given the address of some implementation. - * @param implementation_ The address of some implementation. - */ - constructor(address implementation_) { - if (implementation_ == address(0)) revert(); - - assembly { - sstore(_IMPLEMENTATION_SLOT, implementation_) - } - } - - fallback() external payable virtual { - bytes32 implementation_; - - assembly { - implementation_ := sload(_IMPLEMENTATION_SLOT) - } - - assembly { - calldatacopy(0, 0, calldatasize()) - - let result_ := delegatecall(gas(), implementation_, 0, calldatasize(), 0, 0) - - returndatacopy(0, 0, returndatasize()) - - switch result_ - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } - } - } -} diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 8073ecc..9edf4cc 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -1,20 +1,24 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; +pragma solidity 0.8.26; +import { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol"; import { UIntMath } from "../lib/common/src/libs/UIntMath.sol"; +import { ForcedTransferable } from "../lib/evm-m-extensions/src/components/forcedTransferable/ForcedTransferable.sol"; +import { Freezable } from "../lib/evm-m-extensions/src/components/freezable/Freezable.sol"; +import { Pausable } from "../lib/evm-m-extensions/src/components/pausable/Pausable.sol"; + import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol"; -import { ERC20Extended } from "../lib/common/src/ERC20Extended.sol"; -import { IndexingMath } from "./libs/IndexingMath.sol"; +import { ERC20Extended } from "../lib/common/src/ERC20Extended.sol"; +import { Migratable } from "../lib/common/src/Migratable.sol"; import { IMTokenLike } from "./interfaces/IMTokenLike.sol"; import { IRegistrarLike } from "./interfaces/IRegistrarLike.sol"; +import { ISwapFacilityLike } from "./interfaces/ISwapFacilityLike.sol"; import { IWrappedMToken } from "./interfaces/IWrappedMToken.sol"; -import { Migratable } from "./Migratable.sol"; - /* ██╗ ██╗██████╗ █████╗ ██████╗ ██████╗ ███████╗██████╗ ███╗ ███╗ ████████╗ ██████╗ ██╗ ██╗███████╗███╗ ██╗ @@ -28,28 +32,43 @@ import { Migratable } from "./Migratable.sol"; /** * @title ERC20 Token contract for wrapping M into a non-rebasing token with claimable yields. - * @author M^0 Labs + * @author M0 Labs */ -contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { +contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, Pausable, ForcedTransferable { + /* ============ Structs ============ */ + + /** + * @dev Struct to represent an account's balance and yield earning details. + * @param isEarning Whether the account is actively earning yield. + * @param balance The present amount of tokens held by the account. + * @param earningPrincipal The earning principal for the account. + * @param hasClaimRecipient Whether the account has an explicitly set claim recipient. + */ struct Account { + // First Slot bool isEarning; uint240 balance; - uint128 lastIndex; + // Second slot + uint112 earningPrincipal; + bool hasClaimRecipient; } /* ============ Variables ============ */ - /// @dev Registrar key holding value of whether the earners list can be ignored or not. - bytes32 internal constant _EARNERS_LIST_IGNORED = "earners_list_ignored"; + /// @inheritdoc IWrappedMToken + bytes32 public constant EARNERS_LIST_IGNORED_KEY = "earners_list_ignored"; - /// @dev Registrar key of earners list. - bytes32 internal constant _EARNERS_LIST = "earners"; + /// @inheritdoc IWrappedMToken + bytes32 public constant EARNERS_LIST_NAME = "earners"; - /// @dev Registrar key prefix to determine the override recipient of an account's accrued yield. - bytes32 internal constant _CLAIM_OVERRIDE_RECIPIENT_PREFIX = "wm_claim_override_recipient"; + /// @inheritdoc IWrappedMToken + bytes32 public constant EXCESS_MANAGER_ROLE = keccak256("EXCESS_MANAGER_ROLE"); + + /// @inheritdoc IWrappedMToken + bytes32 public constant CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX = "wm_claim_override_recipient"; - /// @dev Registrar key prefix to determine the migrator contract. - bytes32 internal constant _MIGRATOR_V1_PREFIX = "wm_migrator_v1"; + /// @inheritdoc IWrappedMToken + bytes32 public constant MIGRATOR_KEY_PREFIX = "wm_migrator_v2"; /// @inheritdoc IWrappedMToken address public immutable migrationAdmin; @@ -61,10 +80,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { address public immutable registrar; /// @inheritdoc IWrappedMToken - address public immutable vault; + address public immutable swapFacility; /// @inheritdoc IWrappedMToken - uint112 public principalOfTotalEarningSupply; + uint112 public totalEarningPrincipal; /// @inheritdoc IWrappedMToken uint240 public totalEarningSupply; @@ -75,144 +94,200 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @dev Mapping of accounts to their respective `AccountInfo` structs. mapping(address account => Account balance) internal _accounts; - /// @dev Array of indices at which earning was enabled or disabled. - uint128[] internal _enableDisableEarningIndices; + /// @inheritdoc IWrappedMToken + uint128 public enableMIndex; + + /// @inheritdoc IWrappedMToken + uint128 public disableIndex; + + mapping(address account => address claimRecipient) internal _claimRecipients; + + /// @inheritdoc IWrappedMToken + address public excessDestination; + + /* ============ Modifiers ============ */ + + /// @dev Modifier to check if caller is SwapFacility. + modifier onlySwapFacility() { + if (msg.sender != swapFacility) revert NotSwapFacility(); + _; + } /* ============ Constructor ============ */ /** + * @custom:oz-upgrades-unsafe-allow constructor * @dev Constructs the contract given an M Token address and migration admin. * Note that a proxy will not need to initialize since there are no mutable storage values affected. * @param mToken_ The address of an M Token. + * @param registrar_ The address of a Registrar. + * @param swapFacility_ The address of a Swap Facility. * @param migrationAdmin_ The address of a migration admin. */ - constructor(address mToken_, address migrationAdmin_) ERC20Extended("WrappedM by M^0", "wM", 6) { + constructor( + address mToken_, + address registrar_, + address swapFacility_, + address migrationAdmin_ + ) ERC20Extended("M (Wrapped) by M0", "wM", 6) { + _disableInitializers(); + if ((mToken = mToken_) == address(0)) revert ZeroMToken(); + if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); + if ((swapFacility = swapFacility_) == address(0)) revert ZeroSwapFacility(); if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); - - registrar = IMTokenLike(mToken_).ttgRegistrar(); - vault = IRegistrarLike(registrar).vault(); } - /* ============ Interactive Functions ============ */ + /* ============ Initializer ============ */ - /// @inheritdoc IWrappedMToken - function wrap(address recipient_, uint256 amount_) external returns (uint240 wrapped_) { - return _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); - } + /** + * @dev Initializes the WrappedM token. + * @param admin_ The address of an admin. + * @param freezeManager_ The address of a freeze manager. + * @param pauser_ The address of a pauser. + * @param forcedTransferManager_ The address of a forced transfer manager. + * @param excessManager_ The address of an excess manager. + * @param excessDestination_ The address of an excess destination. + */ + function initialize( + address admin_, + address freezeManager_, + address pauser_, + address forcedTransferManager_, + address excessManager_, + address excessDestination_ + ) public initializer { + __Context_init(); + __ERC165_init(); + __AccessControl_init(); + __Pausable_init(); - /// @inheritdoc IWrappedMToken - function wrap(address recipient_) external returns (uint240 wrapped_) { - return _wrap(msg.sender, recipient_, UIntMath.safe240(IMTokenLike(mToken).balanceOf(msg.sender))); + if (admin_ == address(0)) revert ZeroAdmin(); + _grantRole(DEFAULT_ADMIN_ROLE, admin_); + + if (excessManager_ == address(0)) revert ZeroExcessManager(); + _grantRole(EXCESS_MANAGER_ROLE, excessManager_); + + _setExcessDestination(excessDestination_); + + __Freezable_init(freezeManager_); + __Pausable_init(pauser_); + __ForcedTransferable_init(forcedTransferManager_); } + /* ============ Interactive Functions ============ */ + /// @inheritdoc IWrappedMToken - function unwrap(address recipient_, uint256 amount_) external returns (uint240 unwrapped_) { - return _unwrap(msg.sender, recipient_, UIntMath.safe240(amount_)); + function wrap(address recipient_, uint256 amount_) external onlySwapFacility { + // NOTE: `msg.sender` is always SwapFacility contract. + // `ISwapFacilityLike.msgSender()` is used to ensure that the original caller is passed to `_wrap`. + _wrap(ISwapFacilityLike(msg.sender).msgSender(), recipient_, UIntMath.safe240(amount_)); } /// @inheritdoc IWrappedMToken - function unwrap(address recipient_) external returns (uint240 unwrapped_) { - return _unwrap(msg.sender, recipient_, uint240(balanceWithYieldOf(msg.sender))); + function unwrap(address /* recipient_ */, uint256 amount_) external onlySwapFacility { + // NOTE: `msg.sender` is always SwapFacility contract. + // `ISwapFacilityLike.msgSender()` is used to ensure that the original caller is passed to `_unwrap`. + // NOTE: `recipient` is not used in this function as the $M is always sent to SwapFacility contract. + _unwrap(ISwapFacilityLike(msg.sender).msgSender(), UIntMath.safe240(amount_)); } /// @inheritdoc IWrappedMToken function claimFor(address account_) external returns (uint240 yield_) { - return _claim(account_, currentIndex()); + _requireNotPaused(); + _revertIfFrozen(account_); + + return _claim(account_, currentIndex(), false); } /// @inheritdoc IWrappedMToken - function claimExcess() external returns (uint240 excess_) { - emit ExcessClaimed(excess_ = excess()); + function claimExcess() external returns (uint240 claimed_) { + _requireNotPaused(); + + int256 excess_ = excess(); + + if (excess_ <= 0) return 0; - IMTokenLike(mToken).transfer(vault, excess_); + emit ExcessClaimed(claimed_ = uint240(uint256(excess_))); + + _mint(excessDestination, claimed_); } /// @inheritdoc IWrappedMToken function enableEarning() external { _revertIfNotApprovedEarner(address(this)); - if (isEarningEnabled()) revert EarningIsEnabled(); - // NOTE: This is a temporary measure to prevent re-enabling earning after it has been disabled. - // This line will be removed in the future. - if (wasEarningEnabled()) revert EarningCannotBeReenabled(); - - uint128 currentMIndex_ = _currentMIndex(); - - _enableDisableEarningIndices.push(currentMIndex_); + emit EarningEnabled(enableMIndex = _currentMIndex()); IMTokenLike(mToken).startEarning(); - - emit EarningEnabled(currentMIndex_); } /// @inheritdoc IWrappedMToken function disableEarning() external { _revertIfApprovedEarner(address(this)); - if (!isEarningEnabled()) revert EarningIsDisabled(); - uint128 currentMIndex_ = _currentMIndex(); + emit EarningDisabled(disableIndex = currentIndex()); - _enableDisableEarningIndices.push(currentMIndex_); + delete enableMIndex; IMTokenLike(mToken).stopEarning(); - - emit EarningDisabled(currentMIndex_); } /// @inheritdoc IWrappedMToken function startEarningFor(address account_) external { - _revertIfNotApprovedEarner(account_); - if (!isEarningEnabled()) revert EarningIsDisabled(); - Account storage accountInfo_ = _accounts[account_]; - - if (accountInfo_.isEarning) return; - - // NOTE: Use `currentIndex()` if/when upgrading to support `startEarningFor` while earning is disabled. - uint128 currentIndex_ = _currentMIndex(); - - accountInfo_.isEarning = true; - accountInfo_.lastIndex = currentIndex_; + _startEarningFor(account_, currentIndex()); + } - uint240 balance_ = accountInfo_.balance; + /// @inheritdoc IWrappedMToken + function startEarningFor(address[] calldata accounts_) external { + if (!isEarningEnabled()) revert EarningIsDisabled(); - _addTotalEarningSupply(balance_, currentIndex_); + uint128 currentIndex_ = currentIndex(); - unchecked { - totalNonEarningSupply -= balance_; + for (uint256 index_; index_ < accounts_.length; ++index_) { + _startEarningFor(accounts_[index_], currentIndex_); } - - emit StartedEarning(account_); } /// @inheritdoc IWrappedMToken function stopEarningFor(address account_) external { + _revertIfFrozen(account_); _revertIfApprovedEarner(account_); - uint128 currentIndex_ = currentIndex(); - - _claim(account_, currentIndex_); - - Account storage accountInfo_ = _accounts[account_]; + // NOTE: Skip routing yield to the claim recipient when paused or when that recipient is frozen, so an + // earner cannot block their own deauthorization by pointing yield at a frozen recipient. + // The yield stays on `account_` as balance, mirroring the freeze path in `_beforeFreeze`. + _stopEarningFor(account_, currentIndex(), paused() || isFrozen(claimRecipientFor(account_))); + } - if (!accountInfo_.isEarning) return; + /// @inheritdoc IWrappedMToken + function stopEarningFor(address[] calldata accounts_) external { + uint128 currentIndex_ = currentIndex(); + bool paused_ = paused(); + FreezableStorageStruct storage $ = _getFreezableStorageLocation(); - accountInfo_.isEarning = false; - accountInfo_.lastIndex = 0; + for (uint256 i; i < accounts_.length; ++i) { + _revertIfFrozen($, accounts_[i]); + _revertIfApprovedEarner(accounts_[i]); - uint240 balance_ = accountInfo_.balance; + _stopEarningFor(accounts_[i], currentIndex_, paused_ || isFrozen(claimRecipientFor(accounts_[i]))); + } + } - _subtractTotalEarningSupply(balance_, currentIndex_); + /// @inheritdoc IWrappedMToken + function setClaimRecipient(address claimRecipient_) external { + _accounts[msg.sender].hasClaimRecipient = (_claimRecipients[msg.sender] = claimRecipient_) != address(0); - unchecked { - totalNonEarningSupply += balance_; - } + emit ClaimRecipientSet(msg.sender, claimRecipient_); + } - emit StoppedEarning(account_); + /// @inheritdoc IWrappedMToken + function setExcessDestination(address excessDestination_) external onlyRole(EXCESS_MANAGER_ROLE) { + _setExcessDestination(excessDestination_); } /* ============ Temporary Admin Migration ============ */ @@ -230,9 +305,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function accruedYieldOf(address account_) public view returns (uint240 yield_) { Account storage accountInfo_ = _accounts[account_]; - if (!accountInfo_.isEarning) return 0; - - return _getAccruedYield(accountInfo_.balance, accountInfo_.lastIndex, currentIndex()); + return + accountInfo_.isEarning + ? _getAccruedYield(accountInfo_.balance, accountInfo_.earningPrincipal, currentIndex()) + : 0; } /// @inheritdoc IERC20 @@ -241,30 +317,40 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /// @inheritdoc IWrappedMToken - function balanceWithYieldOf(address account_) public view returns (uint256 balance_) { - return balanceOf(account_) + accruedYieldOf(account_); + function balanceWithYieldOf(address account_) external view returns (uint256 balance_) { + // NOTE: Claiming yield does not necessarily result in the account's new balance equaling the value returned by `balanceWithYieldOf`, + // as the yield may be directed to a claim recipient different from the `account_`. + unchecked { + return balanceOf(account_) + accruedYieldOf(account_); + } } /// @inheritdoc IWrappedMToken - function lastIndexOf(address account_) public view returns (uint128 lastIndex_) { - return _accounts[account_].lastIndex; + function earningPrincipalOf(address account_) external view returns (uint112 earningPrincipal_) { + return _accounts[account_].earningPrincipal; } /// @inheritdoc IWrappedMToken - function claimOverrideRecipientFor(address account_) public view returns (address recipient_) { - return - address( - uint160( - uint256( - IRegistrarLike(registrar).get(keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_PREFIX, account_))) - ) - ) - ); + function claimRecipientFor(address account_) public view returns (address recipient_) { + if (_accounts[account_].hasClaimRecipient) return _claimRecipients[account_]; + + address claimOverrideRecipient_ = address( + uint160(uint256(_getFromRegistrar(keccak256(abi.encode(CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, account_))))) + ); + + return claimOverrideRecipient_ == address(0) ? account_ : claimOverrideRecipient_; } /// @inheritdoc IWrappedMToken function currentIndex() public view returns (uint128 index_) { - return isEarningEnabled() ? _currentMIndex() : _lastDisableEarningIndex(); + uint128 disableIndex_ = disableIndex == 0 ? IndexingMath.EXP_SCALED_ONE : disableIndex; + + unchecked { + return + enableMIndex == 0 + ? disableIndex_ + : UIntMath.safe128((uint256(disableIndex_) * _currentMIndex()) / enableMIndex); + } } /// @inheritdoc IWrappedMToken @@ -274,42 +360,69 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function isEarningEnabled() public view returns (bool isEnabled_) { - return _enableDisableEarningIndices.length % 2 == 1; - } - - /// @inheritdoc IWrappedMToken - function wasEarningEnabled() public view returns (bool wasEarning_) { - return _enableDisableEarningIndices.length != 0; + return enableMIndex != 0; } /// @inheritdoc IWrappedMToken - function excess() public view returns (uint240 excess_) { + function excess() public view returns (int256 excess_) { unchecked { - uint128 currentIndex_ = currentIndex(); - uint240 balance_ = uint240(IMTokenLike(mToken).balanceOf(address(this))); - uint240 earmarked_ = totalNonEarningSupply + _projectedEarningSupply(currentIndex_); + uint256 earmarked_ = totalNonEarningSupply + projectedEarningSupply(); + uint256 balance_ = _mBalanceOf(address(this)); - return balance_ > earmarked_ ? _getSafeTransferableM(balance_ - earmarked_, currentIndex_) : 0; + return int256(balance_) - int256(earmarked_); } } /// @inheritdoc IWrappedMToken function totalAccruedYield() external view returns (uint240 yield_) { - uint240 projectedEarningSupply_ = _projectedEarningSupply(currentIndex()); - uint240 earningSupply_ = totalEarningSupply; - unchecked { - return projectedEarningSupply_ <= earningSupply_ ? 0 : projectedEarningSupply_ - earningSupply_; + return projectedEarningSupply() - totalEarningSupply; } } /// @inheritdoc IERC20 function totalSupply() external view returns (uint256 totalSupply_) { - return totalEarningSupply + totalNonEarningSupply; + unchecked { + return totalEarningSupply + totalNonEarningSupply; + } + } + + /// @inheritdoc IWrappedMToken + function projectedEarningSupply() public view returns (uint240 supply_) { + return + UIntMath.max240( + IndexingMath.getPresentAmountRoundedUp(totalEarningPrincipal, currentIndex()), + totalEarningSupply + ); } /* ============ Internal Interactive Functions ============ */ + /** + * @dev Sets the excess destination to `excessDestination_`. + * @param excessDestination_ The address of the excess destination. + */ + function _setExcessDestination(address excessDestination_) internal { + if (excessDestination_ == address(0)) revert ZeroExcessDestination(); + + emit ExcessDestinationSet(excessDestination = excessDestination_); + } + + /** + * @dev Approve `spender_` to spend `amount_` of tokens from `account_`. + * @param account_ The address approving the allowance. + * @param spender_ The address approved to spend the tokens. + * @param amount_ The amount of tokens being approved for spending. + */ + function _approve(address account_, address spender_, uint256 amount_) internal override { + FreezableStorageStruct storage $ = _getFreezableStorageLocation(); + + _revertIfFrozen($, account_); + _revertIfFrozen($, spender_); + + super._approve(account_, spender_, amount_); + } + /** * @dev Mints `amount_` tokens to `recipient_`. * @param recipient_ The address whose account balance will be incremented. @@ -319,16 +432,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { _revertIfInsufficientAmount(amount_); _revertIfInvalidRecipient(recipient_); - if (_accounts[recipient_].isEarning) { - uint128 currentIndex_ = currentIndex(); - - _claim(recipient_, currentIndex_); - - // NOTE: Additional principal may end up being rounded to 0 and this will not `_revertIfInsufficientAmount`. - _addEarningAmount(recipient_, amount_, currentIndex_); - } else { - _addNonEarningAmount(recipient_, amount_); - } + _accounts[recipient_].isEarning + ? _addEarningAmount(recipient_, amount_, currentIndex()) + : _addNonEarningAmount(recipient_, amount_); emit Transfer(address(0), recipient_, amount_); } @@ -341,16 +447,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function _burn(address account_, uint240 amount_) internal { _revertIfInsufficientAmount(amount_); - if (_accounts[account_].isEarning) { - uint128 currentIndex_ = currentIndex(); - - _claim(account_, currentIndex_); - - // NOTE: Subtracted principal may end up being rounded to 0 and this will not `_revertIfInsufficientAmount`. - _subtractEarningAmount(account_, amount_, currentIndex_); - } else { - _subtractNonEarningAmount(account_, amount_); - } + _accounts[account_].isEarning + ? _subtractEarningAmount(account_, amount_, currentIndex()) + : _subtractNonEarningAmount(account_, amount_); emit Transfer(account_, address(0), amount_); } @@ -374,92 +473,102 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param amount_ The present amount of tokens to decrement by. */ function _subtractNonEarningAmount(address account_, uint240 amount_) internal { - unchecked { - Account storage accountInfo_ = _accounts[account_]; - - uint240 balance_ = accountInfo_.balance; + Account storage accountInfo_ = _accounts[account_]; + uint240 balance_ = accountInfo_.balance; - if (balance_ < amount_) revert InsufficientBalance(account_, balance_, amount_); + if (balance_ < amount_) revert InsufficientBalance(account_, balance_, amount_); + unchecked { accountInfo_.balance = balance_ - amount_; totalNonEarningSupply -= amount_; } } /** - * @dev Increments the token balance of `account_` by `amount_`, assuming earning status and updated index. + * @dev Increments the token balance of `account_` by `amount_`, assuming earning status. * @param account_ The address whose account balance will be incremented. * @param amount_ The present amount of tokens to increment by. * @param currentIndex_ The current index to use to compute the principal amount. */ function _addEarningAmount(address account_, uint240 amount_, uint128 currentIndex_) internal { + Account storage accountInfo_ = _accounts[account_]; + + // NOTE: Tracks two principal amounts: rounded up and rounded down. + // Slightly overestimates the principal of total earning supply to provide extra safety in `excess` calculations. + uint112 principalUp_ = IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_); + uint112 principalDown_ = IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_); + // NOTE: Can be `unchecked` because the max amount of wrappable M is never greater than `type(uint240).max`. unchecked { - _accounts[account_].balance += amount_; - _addTotalEarningSupply(amount_, currentIndex_); + accountInfo_.balance += amount_; + accountInfo_.earningPrincipal = UIntMath.safe112(uint256(accountInfo_.earningPrincipal) + principalDown_); } + + _addTotalEarningSupply(amount_, principalUp_); } /** - * @dev Decrements the token balance of `account_` by `amount_`, assuming earning status and updated index. + * @dev Decrements the token balance of `account_` by `amount_`, assuming earning status. * @param account_ The address whose account balance will be decremented. * @param amount_ The present amount of tokens to decrement by. * @param currentIndex_ The current index to use to compute the principal amount. */ function _subtractEarningAmount(address account_, uint240 amount_, uint128 currentIndex_) internal { - unchecked { - Account storage accountInfo_ = _accounts[account_]; + Account storage accountInfo_ = _accounts[account_]; + uint240 balance_ = accountInfo_.balance; - uint240 balance_ = accountInfo_.balance; + if (balance_ < amount_) revert InsufficientBalance(account_, balance_, amount_); - if (balance_ < amount_) revert InsufficientBalance(account_, balance_, amount_); + uint112 earningPrincipal_ = accountInfo_.earningPrincipal; + // NOTE: Tracks two principal amounts: rounded up and rounded down. + // Slightly overestimates the principal of total earning supply to provide extra safety in `excess` calculations. + uint112 principalUp_ = IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_); + uint112 principalDown_ = IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_); + + unchecked { accountInfo_.balance = balance_ - amount_; - _subtractTotalEarningSupply(amount_, currentIndex_); + // `min112` prevents `earningPrincipal` underflow. + accountInfo_.earningPrincipal = earningPrincipal_ - UIntMath.min112(principalUp_, earningPrincipal_); } + + _subtractTotalEarningSupply(amount_, principalDown_); } /** * @dev Claims accrued yield for `account_` given a `currentIndex_`. * @param account_ The address to claim accrued yield for. * @param currentIndex_ The current index to accrue until. + * @param skipTransfer_ Whether to skip transferring yield to a non-self claim recipient. * @return yield_ The accrued yield that was claimed. */ - function _claim(address account_, uint128 currentIndex_) internal returns (uint240 yield_) { + function _claim(address account_, uint128 currentIndex_, bool skipTransfer_) internal returns (uint240 yield_) { Account storage accountInfo_ = _accounts[account_]; if (!accountInfo_.isEarning) return 0; - uint128 index_ = accountInfo_.lastIndex; - - if (currentIndex_ == index_) return 0; - uint240 startingBalance_ = accountInfo_.balance; - yield_ = _getAccruedYield(startingBalance_, index_, currentIndex_); + yield_ = _getAccruedYield(startingBalance_, accountInfo_.earningPrincipal, currentIndex_); - accountInfo_.lastIndex = currentIndex_; + if (yield_ == 0) return 0; unchecked { - if (yield_ == 0) return 0; - + // Update balance and total earning supply to account for the yield, but the principals have not changed. accountInfo_.balance = startingBalance_ + yield_; - - // Update the total earning supply to account for the yield, but the principal has not changed. totalEarningSupply += yield_; } - address claimOverrideRecipient_ = claimOverrideRecipientFor(account_); - address claimRecipient_ = claimOverrideRecipient_ == address(0) ? account_ : claimOverrideRecipient_; + // When transferring is skipped, the yield stays on `account_`, so it is the effective recipient. + address claimRecipient_ = skipTransfer_ ? account_ : claimRecipientFor(account_); // Emit the appropriate `Claimed` and `Transfer` events, depending on the claim override recipient emit Claimed(account_, claimRecipient_, yield_); emit Transfer(address(0), account_, yield_); - if (claimRecipient_ != account_) { - // NOTE: Watch out for a long chain of earning claim override recipients. - _transfer(account_, claimRecipient_, yield_, currentIndex_); - } + if (claimRecipient_ == account_) return yield_; + + _transfer(account_, claimRecipient_, yield_, currentIndex_); } /** @@ -470,40 +579,44 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param currentIndex_ The current index. */ function _transfer(address sender_, address recipient_, uint240 amount_, uint128 currentIndex_) internal { + _requireNotPaused(); _revertIfInvalidRecipient(recipient_); - // Claims for both the sender and recipient are required before transferring since add an subtract functions - // assume accounts' balances are up-to-date with the current index. - _claim(sender_, currentIndex_); - _claim(recipient_, currentIndex_); + FreezableStorageStruct storage $ = _getFreezableStorageLocation(); + + _revertIfFrozen($, msg.sender); + _revertIfFrozen($, sender_); + _revertIfFrozen($, recipient_); emit Transfer(sender_, recipient_, amount_); - Account storage senderAccountInfo_ = _accounts[sender_]; - Account storage recipientAccountInfo_ = _accounts[recipient_]; + if (amount_ == 0) return; - // If the sender and recipient are both earning or both non-earning, update their balances without affecting - // the total earning and non-earning supply storage variables. - if (senderAccountInfo_.isEarning == recipientAccountInfo_.isEarning) { - uint240 senderBalance_ = senderAccountInfo_.balance; + Account storage senderInfo_ = _accounts[sender_]; + Account storage recipientInfo_ = _accounts[recipient_]; - if (senderBalance_ < amount_) revert InsufficientBalance(sender_, senderBalance_, amount_); + bool senderIsEarner_ = senderInfo_.isEarning; + bool recipientIsEarner_ = recipientInfo_.isEarning; - unchecked { - senderAccountInfo_.balance = senderBalance_ - amount_; - recipientAccountInfo_.balance += amount_; - } + // If sender and earner are different earner states, transfer affects total supplies. + if (senderIsEarner_ != recipientIsEarner_) { + senderIsEarner_ + ? _subtractEarningAmount(sender_, amount_, currentIndex_) + : _subtractNonEarningAmount(sender_, amount_); + + recipientIsEarner_ + ? _addEarningAmount(recipient_, amount_, currentIndex_) + : _addNonEarningAmount(recipient_, amount_); return; } - senderAccountInfo_.isEarning - ? _subtractEarningAmount(sender_, amount_, currentIndex_) - : _subtractNonEarningAmount(sender_, amount_); + if (senderInfo_.balance < amount_) revert InsufficientBalance(sender_, senderInfo_.balance, amount_); - recipientAccountInfo_.isEarning - ? _addEarningAmount(recipient_, amount_, currentIndex_) - : _addNonEarningAmount(recipient_, amount_); + // If sender and recipient are both earners or both non-earners, transfer does not affect total supplies. + senderIsEarner_ + ? _transferBetweenEarners(senderInfo_, recipientInfo_, amount_, currentIndex_) + : _transferBetweenNonEarners(senderInfo_, recipientInfo_, amount_); } /** @@ -516,83 +629,227 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { _transfer(sender_, recipient_, UIntMath.safe240(amount_), currentIndex()); } + /** + * @dev Transfers `amount_` tokens between earners given some current index. + * @param sender_ The sender's Account storage pointer. + * @param recipient_ The recipient's Account storage pointer. + * @param amount_ The amount to be transferred. + * @param currentIndex_ The current index. + */ + function _transferBetweenEarners( + Account storage sender_, + Account storage recipient_, + uint240 amount_, + uint128 currentIndex_ + ) internal { + uint112 earningPrincipal_ = sender_.earningPrincipal; + + // `min112` prevents `earningPrincipal` underflow. + uint112 principal_ = UIntMath.min112( + IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_), + earningPrincipal_ + ); + + // NOTE: Can be `unchecked` because `_transfer` already checked for insufficient sender balance. + unchecked { + sender_.balance -= amount_; + sender_.earningPrincipal = earningPrincipal_ - principal_; + + recipient_.balance += amount_; + recipient_.earningPrincipal = UIntMath.safe112(uint256(recipient_.earningPrincipal) + principal_); + } + } + + /** + * @dev Transfers `amount_` tokens between non-earners. + * @param sender_ The sender's Account storage pointer. + * @param recipient_ The recipient's Account storage pointer. + * @param amount_ The amount to be transferred. + */ + function _transferBetweenNonEarners(Account storage sender_, Account storage recipient_, uint240 amount_) internal { + // NOTE: Can be `unchecked` because `_transfer` already checked for insufficient sender balance. + unchecked { + sender_.balance -= amount_; + recipient_.balance += amount_; + } + } + /** * @dev Increments total earning supply by `amount_` tokens. - * @param amount_ The present amount of tokens to increment total earning supply by. - * @param currentIndex_ The current index used to compute the principal amount. + * @param amount_ The present amount of tokens to increment total earning supply by. + * @param principal_ The principal amount of tokens to increment total earning principal by. */ - function _addTotalEarningSupply(uint240 amount_, uint128 currentIndex_) internal { + function _addTotalEarningSupply(uint240 amount_, uint112 principal_) internal { unchecked { // Increment the total earning supply and principal proportionally. totalEarningSupply += amount_; - principalOfTotalEarningSupply += IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_); + totalEarningPrincipal = UIntMath.safe112(uint256(totalEarningPrincipal) + principal_); } } /** * @dev Decrements total earning supply by `amount_` tokens. - * @param amount_ The present amount of tokens to decrement total earning supply by. - * @param currentIndex_ The current index used to compute the principal amount. + * @param amount_ The present amount of tokens to decrement total earning supply by. + * @param principal_ The principal amount of tokens to decrement total earning principal by. */ - function _subtractTotalEarningSupply(uint240 amount_, uint128 currentIndex_) internal { - if (amount_ >= totalEarningSupply) { - totalEarningSupply = 0; - principalOfTotalEarningSupply = 0; - - return; - } + function _subtractTotalEarningSupply(uint240 amount_, uint112 principal_) internal { + uint240 totalEarningSupply_ = totalEarningSupply; + uint112 totalEarningPrincipal_ = totalEarningPrincipal; unchecked { - uint112 principal_ = IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_); - - principalOfTotalEarningSupply -= ( - principal_ > principalOfTotalEarningSupply ? principalOfTotalEarningSupply : principal_ - ); - - totalEarningSupply -= amount_; + // `min240` and `min112` prevent `totalEarningSupply` and `totalEarningPrincipal` underflow respectively. + totalEarningSupply = totalEarningSupply_ - UIntMath.min240(amount_, totalEarningSupply_); + totalEarningPrincipal = totalEarningPrincipal_ - UIntMath.min112(principal_, totalEarningPrincipal_); } } /** * @dev Wraps `amount` M from `account_` into wM for `recipient`. - * @param account_ The account from which M is deposited. + * @param account_ The account depositing M. * @param recipient_ The account receiving the minted wM. * @param amount_ The amount of M deposited. - * @return wrapped_ The amount of wM minted. */ - function _wrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 wrapped_) { - uint256 startingBalance_ = IMTokenLike(mToken).balanceOf(address(this)); + function _wrap(address account_, address recipient_, uint240 amount_) internal { + _requireNotPaused(); + FreezableStorageStruct storage $ = _getFreezableStorageLocation(); + + _revertIfFrozen($, account_); + _revertIfFrozen($, recipient_); + + // NOTE: Always transfer from SwapFacility as it is the only contract that can call this function. // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. - IMTokenLike(mToken).transferFrom(account_, address(this), amount_); + IMTokenLike(mToken).transferFrom(msg.sender, address(this), amount_); - // NOTE: When this WrappedMToken contract is earning, any amount of M sent to it is converted to a principal - // amount at the MToken contract, which when represented as a present amount, may be a rounding error - // amount less than `amount_`. In order to capture the real increase in M, the difference between the - // starting and ending M balance is minted as WrappedM. - _mint(recipient_, wrapped_ = UIntMath.safe240(IMTokenLike(mToken).balanceOf(address(this)) - startingBalance_)); + // NOTE: Mints precise amount of Wrapped $M to `recipient_`. + // Option 1: $M transfer from an $M earner to another $M earner (`WrappedM` in earning state): rounds up → rounds up, + // 0, 1, or XX extra wei may be locked in `WrappedM` compared to the minted amount of Wrapped $M. + // Option 2: $M transfer from an $M non-earner to an $M earner (`WrappedM` in earning state): precise $M transfer → rounds down, + // 0, -1, or -XX wei may be locked in `WrappedM` compared to the minted amount of Wrapped $M. + _mint(recipient_, amount_); } /** - * @dev Unwraps `amount` wM from `account_` into M for `recipient`. - * @param account_ The account from which WM is burned. - * @param recipient_ The account receiving the withdrawn M. - * @param amount_ The amount of wM burned. - * @return unwrapped_ The amount of M withdrawn. + * @dev Unwraps `amount` wM from `account_` into M. + * @param account_ The account whose wM is being burned. + * @param amount_ The amount of wM burned. */ - function _unwrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 unwrapped_) { - _burn(account_, amount_); + function _unwrap(address account_, uint240 amount_) internal { + _requireNotPaused(); - uint256 startingBalance_ = IMTokenLike(mToken).balanceOf(address(this)); + // NOTE: No recipient frozen check here. The final recipient receives $M (not Wrapped $M) + // directly from SwapFacility, so a Wrapped $M frozen guard would have no effect. + _revertIfFrozen(account_); + + // NOTE: Always burn from SwapFacility as it is the only contract that can call this function. + _burn(msg.sender, amount_); // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. - IMTokenLike(mToken).transfer(recipient_, _getSafeTransferableM(amount_, currentIndex())); + // NOTE: Computes the actual decrease in the $M balance of the `WrappedM` contract. + // Option 1: $M transfer from an $M earner (`WrappedM` in earning state) to another $M earner: round up → rounds up. + // Option 2: $M transfer from an $M earner (`WrappedM` in earning state) to an $M non-earner: round up → precise $M transfer. + // In both cases, 0, 1, or XX extra wei may be deducted from the `WrappedM` contract's $M balance compared to the burned amount of Wrapped $M. + IMTokenLike(mToken).transfer(msg.sender, amount_); + } + + /** + * @dev Starts earning for `account` if allowed by the Registrar. + * @param account_ The account to start earning for. + * @param currentIndex_ The current index. + */ + function _startEarningFor(address account_, uint128 currentIndex_) internal { + _requireNotPaused(); + _revertIfFrozen(account_); + _revertIfNotApprovedEarner(account_); + + Account storage accountInfo_ = _accounts[account_]; + + if (accountInfo_.isEarning) return; + + uint240 balance_ = accountInfo_.balance; + + // NOTE: Tracks two principal amounts: rounded up and rounded down. + // Slightly overestimates the principal of total earning supply to provide extra safety in `excess` calculations. + uint112 principalUp_ = IndexingMath.getPrincipalAmountRoundedUp(balance_, currentIndex_); + uint112 principalDown_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); + + accountInfo_.isEarning = true; + accountInfo_.earningPrincipal = principalDown_; + + _addTotalEarningSupply(balance_, principalUp_); + + unchecked { + totalNonEarningSupply -= balance_; + } - // NOTE: When this WrappedMToken contract is earning, any amount of M sent from it is converted to a principal - // amount at the MToken contract, which when represented as a present amount, may be a rounding error - // amount more than `amount_`. In order to capture the real decrease in M, the difference between the - // ending and starting M balance is returned. - return UIntMath.safe240(startingBalance_ - IMTokenLike(mToken).balanceOf(address(this))); + emit StartedEarning(account_); + } + + /** + * @dev Stops earning for `account` given some current index. + * @param account_ The account to stop earning for. + * @param currentIndex_ The current index. + * @param skipTransfer_ Whether to skip routing the claimed yield to a non-self claim recipient. + */ + function _stopEarningFor(address account_, uint128 currentIndex_, bool skipTransfer_) internal { + Account storage accountInfo_ = _accounts[account_]; + + if (!accountInfo_.isEarning) return; + + _claim(account_, currentIndex_, skipTransfer_); + + uint240 balance_ = accountInfo_.balance; + uint112 earningPrincipal_ = accountInfo_.earningPrincipal; + + delete accountInfo_.isEarning; + delete accountInfo_.earningPrincipal; + + _subtractTotalEarningSupply(balance_, earningPrincipal_); + + unchecked { + totalNonEarningSupply += balance_; + } + + emit StoppedEarning(account_); + } + + /** + * @dev Hook called before freezing an account. Claims accrued yield and stops earning. + * Routing to a claim recipient is skipped so freezing never reverts on a frozen + * recipient and the yield stays on the frozen account, seizable via `forceTransfer`. + * @param account_ The account about to be frozen. + */ + function _beforeFreeze(address account_) internal override { + _stopEarningFor(account_, currentIndex(), true); + + super._beforeFreeze(account_); + } + + /** + * @dev Forcefully transfers `amount_` tokens from `frozenAccount_` to `recipient_`. + * Bypasses pause (compliance > pause). Frozen account is guaranteed non-earning + * thanks to `_beforeFreeze`. + * @param frozenAccount_ The frozen account from which tokens are seized. + * @param recipient_ The recipient's address. + * @param amount_ The amount to be transferred. + */ + function _forceTransfer(address frozenAccount_, address recipient_, uint256 amount_) internal override { + _revertIfInvalidRecipient(recipient_); + _revertIfNotFrozen(frozenAccount_); + _revertIfFrozen(recipient_); + + uint240 amount240_ = UIntMath.safe240(amount_); + + emit Transfer(frozenAccount_, recipient_, amount240_); + emit ForcedTransfer(frozenAccount_, recipient_, msg.sender, amount240_); + + if (amount240_ == 0) return; + + _subtractNonEarningAmount(frozenAccount_, amount240_); + + _accounts[recipient_].isEarning + ? _addEarningAmount(recipient_, amount240_, currentIndex()) + : _addNonEarningAmount(recipient_, amount240_); } /* ============ Internal View/Pure Functions ============ */ @@ -602,27 +859,30 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return IMTokenLike(mToken).currentIndex(); } - /// @dev Returns the earning index from the last `disableEarning` call. - function _lastDisableEarningIndex() internal view returns (uint128 index_) { - return wasEarningEnabled() ? _unsafeAccess(_enableDisableEarningIndices, 1) : 0; + /** + * @dev Returns whether `account_` is a TTG-approved earner. + * @param account_ The account being queried. + * @return isApproved_ True if the account_ is a TTG-approved earner, false otherwise. + */ + function _isApprovedEarner(address account_) internal view returns (bool isApproved_) { + return + IRegistrarLike(registrar).get(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || + IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, account_); } /** - * @dev Compute the yield given an account's balance, last index, and the current index. - * @param balance_ The token balance of an earning account. - * @param lastIndex_ The index of ast interaction for the account. - * @param currentIndex_ The current index. - * @return yield_ The yield accrued since the last interaction. + * @dev Compute the yield given an account's balance, earning principal, and the current index. + * @param balance_ The token balance of an earning account. + * @param earningPrincipal_ The earning principal of the account. + * @param currentIndex_ The current index. + * @return yield_ The yield accrued since the last interaction. */ function _getAccruedYield( uint240 balance_, - uint128 lastIndex_, + uint112 earningPrincipal_, uint128 currentIndex_ ) internal pure returns (uint240 yield_) { - uint240 balanceWithYield_ = IndexingMath.getPresentAmountRoundedDown( - IndexingMath.getPrincipalAmountRoundedDown(balance_, lastIndex_), - currentIndex_ - ); + uint240 balanceWithYield_ = IndexingMath.getPresentAmountRoundedDown(earningPrincipal_, currentIndex_); unchecked { return (balanceWithYield_ <= balance_) ? 0 : balanceWithYield_ - balance_; @@ -630,20 +890,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /** - * @dev Compute the adjusted amount of M that can safely be transferred out given the current index. - * @param amount_ Some amount to be transferred out of the wrapper. - * @param currentIndex_ The current index. - * @return safeAmount_ The adjusted amount that can safely be transferred out. + * @dev Retrieve a value from the Registrar. + * @param key_ The key to retrieve the value for. + * @return value_ The value stored in the Registrar. */ - function _getSafeTransferableM(uint240 amount_, uint128 currentIndex_) internal view returns (uint240 safeAmount_) { - // If the wrapper is earning, adjust `amount_` to ensure it's M balance decrement is limited to `amount_`. - return - IMTokenLike(mToken).isEarning(address(this)) - ? IndexingMath.getPresentAmountRoundedDown( - IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_), - currentIndex_ - ) - : amount_; + function _getFromRegistrar(bytes32 key_) internal view returns (bytes32 value_) { + return IRegistrarLike(registrar).get(key_); } /// @dev Returns the address of the contract to use as a migrator, if any. @@ -652,29 +904,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { address( uint160( // NOTE: A subsequent implementation should use a unique migrator prefix. - uint256(IRegistrarLike(registrar).get(keccak256(abi.encode(_MIGRATOR_V1_PREFIX, address(this))))) + uint256(_getFromRegistrar(keccak256(abi.encode(MIGRATOR_KEY_PREFIX, address(this))))) ) ); } /** - * @dev Returns whether `account_` is a TTG-approved earner. - * @param account_ The account being queried. - * @return isApproved_ True if the account_ is a TTG-approved earner, false otherwise. - */ - function _isApprovedEarner(address account_) internal view returns (bool isApproved_) { - return - IRegistrarLike(registrar).get(_EARNERS_LIST_IGNORED) != bytes32(0) || - IRegistrarLike(registrar).listContains(_EARNERS_LIST, account_); - } - - /** - * @dev Returns the projected total earning supply if all accrued yield was claimed at this moment. - * @param currentIndex_ The current index. - * @return supply_ The projected total earning supply. + * @dev Returns the M Token balance of `account_`. + * @param account_ The account being queried. + * @return balance_ The M Token balance of the account. */ - function _projectedEarningSupply(uint128 currentIndex_) internal view returns (uint240 supply_) { - return IndexingMath.getPresentAmountRoundedDown(principalOfTotalEarningSupply, currentIndex_); + function _mBalanceOf(address account_) internal view returns (uint240 balance_) { + // NOTE: M Token balance are limited to `uint240`. + return uint240(IMTokenLike(mToken).balanceOf(account_)); } /** @@ -686,11 +928,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /** - * @dev Reverts if `recipient_` is address(0). - * @param recipient_ Address of a recipient. + * @dev Reverts if `account_` is address(0). + * @param account_ Address of an account. */ - function _revertIfInvalidRecipient(address recipient_) internal pure { - if (recipient_ == address(0)) revert InvalidRecipient(recipient_); + function _revertIfInvalidRecipient(address account_) internal pure { + if (account_ == address(0)) revert InvalidRecipient(account_); } /** @@ -698,7 +940,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param account_ Address of an account. */ function _revertIfApprovedEarner(address account_) internal view { - if (_isApprovedEarner(account_)) revert IsApprovedEarner(); + if (_isApprovedEarner(account_)) revert IsApprovedEarner(account_); } /** @@ -706,25 +948,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param account_ Address of an account. */ function _revertIfNotApprovedEarner(address account_) internal view { - if (!_isApprovedEarner(account_)) revert NotApprovedEarner(); - } - - /** - * @dev Reads the uint128 value at some index of an array of uint128 values whose storage pointer is given, - * assuming the index is valid, without wasting gas checking for out-of-bounds errors. - * @param array_ The storage pointer of an array of uint128 values. - * @param i_ The index of the array to read. - */ - function _unsafeAccess(uint128[] storage array_, uint256 i_) internal view returns (uint128 value_) { - assembly { - mstore(0, array_.slot) - - value_ := sload(add(keccak256(0, 0x20), div(i_, 2))) - - // Since uint128 values take up either the top half or bottom half of a slot, shift the result accordingly. - if eq(mod(i_, 2), 1) { - value_ := shr(128, value_) - } - } + if (!_isApprovedEarner(account_)) revert NotApprovedEarner(account_); } } diff --git a/src/WrappedMTokenMigratorV1.sol b/src/WrappedMTokenMigratorV1.sol new file mode 100644 index 0000000..e4a8399 --- /dev/null +++ b/src/WrappedMTokenMigratorV1.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.26; + +import { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol"; + +import { ListOfEarnersToMigrate } from "./ListOfEarnersToMigrate.sol"; + +/** + * @title Migrator contract for migrating a WrappedMToken contract from V1 to V2. + * @author M0 Labs + */ +contract WrappedMTokenMigratorV1 { + /// @notice Emitted when the `enableDisableEarningIndices` array has an invalid length. + error InvalidEnableDisableEarningIndicesArrayLength(); + + /* ============ Structs ============ */ + + /** + * @dev Struct to represent an account's balance and yield earning details with last index (prior version). + * @param isEarning Whether the account is actively earning yield. + * @param balance The present amount of tokens held by the account. + * @param lastIndex The index of the last interaction for the account (0 for non-earning accounts). + */ + struct IndexBasedAccount { + // First Slot + bool isEarning; + uint240 balance; + // Second slot + uint128 lastIndex; + } + + /** + * @dev Struct to represent an account's balance and yield earning details. + * @param isEarning Whether the account is actively earning yield. + * @param balance The present amount of tokens held by the account. + * @param earningPrincipal The earning principal for the account (0 for non-earning accounts). + */ + struct Account { + // First Slot + bool isEarning; + uint240 balance; + // Second slot + uint112 earningPrincipal; + } + + /// @dev Storage slot with the address of the current factory. `keccak256('eip1967.proxy.implementation') - 1`. + uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + address public immutable newImplementation; + + address public immutable listOfEarnerToMigrate; + + address public immutable admin; + address public immutable freezeManager; + address public immutable pauser; + address public immutable forcedTransferManager; + address public immutable excessManager; + address public immutable excessDestination; + + constructor( + address newImplementation_, + address[] memory earners_, + address admin_, + address freezeManager_, + address pauser_, + address forcedTransferManager_, + address excessManager_, + address excessDestination_ + ) { + newImplementation = newImplementation_; + + listOfEarnerToMigrate = address(new ListOfEarnersToMigrate(earners_)); + + admin = admin_; + freezeManager = freezeManager_; + pauser = pauser_; + forcedTransferManager = forcedTransferManager_; + excessManager = excessManager_; + excessDestination = excessDestination_; + } + + fallback() external virtual { + _migrateEarners(); + + (bool earningEnabled_, uint128 disableIndex_) = _clearEnableDisableEarningIndices(); + + if (earningEnabled_) { + _setEnableMIndex(IndexingMath.EXP_SCALED_ONE); + } else { + _setDisableIndex(disableIndex_); + } + + address newImplementation_ = newImplementation; + + assembly { + sstore(_IMPLEMENTATION_SLOT, newImplementation_) + } + + (bool success_, ) = address(this).call( + abi.encodeWithSelector( + bytes4(keccak256("initialize(address,address,address,address,address,address)")), + admin, + freezeManager, + pauser, + forcedTransferManager, + excessManager, + excessDestination + ) + ); + + require(success_, "Initialize call failed"); + } + + function _migrateEarners() internal { + address[] memory earners_ = ListOfEarnersToMigrate(listOfEarnerToMigrate).getEarners(); + + mapping(address => Account) storage accounts_ = _getAccounts(); + + uint256 index_ = earners_.length; + + while (index_ > 0) { + Account storage accountInfo_ = accounts_[earners_[--index_]]; + + if (!accountInfo_.isEarning) continue; + + IndexBasedAccount storage accountInfoV1_; + + assembly { + accountInfoV1_.slot := accountInfo_.slot + } + + uint128 lastIndex_ = accountInfoV1_.lastIndex; + + delete accountInfoV1_.lastIndex; + + accountInfo_.earningPrincipal = IndexingMath.getPrincipalAmountRoundedDown( + accountInfoV1_.balance, + lastIndex_ + ); + } + } + + function _getAccounts() internal pure returns (mapping(address => Account) storage accounts_) { + assembly { + accounts_.slot := 6 // `_accounts` is slot 6 in v1. + } + } + + /** + * @dev Clears the entire `_enableDisableEarningIndices` array in storage, returning useful information. + * @return earningEnabled_ Whether earning is enabled. + * @return disableIndex_ The index when earning was disabled, if any. + */ + function _clearEnableDisableEarningIndices() internal returns (bool earningEnabled_, uint128 disableIndex_) { + uint128[] storage array_; + + assembly { + array_.slot := 7 // `_enableDisableEarningIndices` was slot 7 in v1. + } + + // If the array is empty, earning is disabled and thus the disable index was non-existent. + if (array_.length == 0) return (false, 0); + + // If the array has one element, earning is enabled and the disable index is non-existent. + if (array_.length == 1) { + array_.pop(); + return (true, 0); + } + + // If the array has two elements, earning is disabled and the disable index is the second element. + if (array_.length == 2) { + disableIndex_ = array_[1]; + array_.pop(); + array_.pop(); + return (false, disableIndex_); + } + + // In v1, it is not possible for the `_enableDisableEarningIndices` array to have more than two elements. + revert InvalidEnableDisableEarningIndicesArrayLength(); + } + + /** + * @dev Sets the `enableMIndex` slot to `index_`. + * @param index_ The index to set the `enableMIndex . + */ + function _setEnableMIndex(uint128 index_) internal { + assembly { + sstore(7, index_) // `enableMIndex` is the lower half of slot 7 in v2. + } + } + + /** + * @dev Sets the `disableIndex` slot to `index_`. + * @param index_ The index to set the `disableIndex . + */ + function _setDisableIndex(uint128 index_) internal { + assembly { + sstore(7, shl(128, index_)) // `disableIndex` is the upper half of slot 7 in v2. + } + } +} diff --git a/src/interfaces/IMTokenLike.sol b/src/interfaces/IMTokenLike.sol index e0c7939..2c22e59 100644 --- a/src/interfaces/IMTokenLike.sol +++ b/src/interfaces/IMTokenLike.sol @@ -1,14 +1,44 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; +pragma solidity 0.8.26; /** * @title Subset of M Token interface required for source contracts. - * @author M^0 Labs + * @author M0 Labs */ interface IMTokenLike { /* ============ Interactive Functions ============ */ + /** + * @notice Approves `spender` to spend up to `amount` of the token balance of `owner`, via a signature. + * @param owner The address of the account who's token balance is being approved to be spent by `spender`. + * @param spender The address of an account allowed to spend on behalf of `owner`. + * @param value The amount of the allowance being approved. + * @param deadline The last timestamp where the signature is still valid. + * @param v An ECDSA secp256k1 signature parameter (EIP-2612 via EIP-712). + * @param r An ECDSA secp256k1 signature parameter (EIP-2612 via EIP-712). + * @param s An ECDSA secp256k1 signature parameter (EIP-2612 via EIP-712). + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @notice Approves `spender` to spend up to `amount` of the token balance of `owner`, via a signature. + * @param owner The address of the account who's token balance is being approved to be spent by `spender`. + * @param spender The address of an account allowed to spend on behalf of `owner`. + * @param value The amount of the allowance being approved. + * @param deadline The last timestamp where the signature is still valid. + * @param signature An arbitrary signature (EIP-712). + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, bytes memory signature) external; + /** * @notice Allows a calling account to transfer `amount` tokens to `recipient`. * @param recipient The address of the recipient who's token balance will be incremented. @@ -26,7 +56,7 @@ interface IMTokenLike { */ function transferFrom(address sender, address recipient, uint256 amount) external returns (bool success); - /// @notice Starts earning for caller if allowed by TTG. + /// @notice Starts earning for caller if allowed by the Registrar. function startEarning() external; /// @notice Stops earning for caller. @@ -51,6 +81,10 @@ interface IMTokenLike { /// @notice The current index that would be written to storage if `updateIndex` is called. function currentIndex() external view returns (uint128 currentIndex); - /// @notice The address of the TTG Registrar contract. - function ttgRegistrar() external view returns (address ttgRegistrar); + /** + * @notice The principal of an earner M token balance. + * @param account The account to get the principal balance of. + * @return The principal balance of the account. + */ + function principalBalanceOf(address account) external view returns (uint240); } diff --git a/src/interfaces/IMigratable.sol b/src/interfaces/IMigratable.sol deleted file mode 100644 index 94177a1..0000000 --- a/src/interfaces/IMigratable.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.23; - -/** - * @title Interface for exposing the ability to migrate a contract, extending the ERC-1967 interface. - * @author M^0 Labs - */ -interface IMigratable { - /* ============ Events ============ */ - - /** - * @notice Emitted when a migration to a new implementation is performed. - * @param migrator The address that performed the migration. - * @param oldImplementation The address of the old implementation. - * @param newImplementation The address of the new implementation. - */ - event Migrated(address indexed migrator, address indexed oldImplementation, address indexed newImplementation); - - /** - * @notice Emitted when the implementation address for the proxy is changed. - * @param implementation The address of the new implementation for the proxy. - */ - event Upgraded(address indexed implementation); - - /// @notice Emitted when calling `stopEarning` for an account approved as earner by TTG. - error InvalidMigrator(); - - /// @notice Emitted when the delegatecall to a migrator fails. - error MigrationFailed(); - - /// @notice Emitted when the zero address is passed as a migrator. - error ZeroMigrator(); - - /* ============ Interactive Functions ============ */ - - /// @notice Performs an arbitrarily defined migration. - function migrate() external; - - /* ============ View/Pure Functions ============ */ - - /// @notice Returns the address of the current implementation contract. - function implementation() external view returns (address implementation); -} diff --git a/src/interfaces/IRegistrarLike.sol b/src/interfaces/IRegistrarLike.sol index 2c08775..8b6acbb 100644 --- a/src/interfaces/IRegistrarLike.sol +++ b/src/interfaces/IRegistrarLike.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; +pragma solidity 0.8.26; /** * @title Subset of Registrar interface required for source contracts. - * @author M^0 Labs + * @author M0 Labs */ interface IRegistrarLike { /* ============ View/Pure Functions ============ */ @@ -23,7 +23,4 @@ interface IRegistrarLike { * @return contains Whether `list` contains `account` or not. */ function listContains(bytes32 list, address account) external view returns (bool contains); - - /// @notice Returns the address of the Vault. - function vault() external view returns (address vault); } diff --git a/src/interfaces/ISwapFacilityLike.sol b/src/interfaces/ISwapFacilityLike.sol new file mode 100644 index 0000000..a876b45 --- /dev/null +++ b/src/interfaces/ISwapFacilityLike.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.26; + +/** + * @title Subset of SwapFacility interface. + * @author M0 Labs + */ +interface ISwapFacilityLike { + /* ============ Interactive Functions ============ */ + + /** + * @notice Swaps $M token to $M Extension. + * @param extensionOut The address of the M Extension to swap to. + * @param amount The amount of $M token to swap. + * @param recipient The address to receive the swapped $M Extension tokens. + */ + function swapInM(address extensionOut, uint256 amount, address recipient) external; + + /** + * @notice Swaps $M Extension to $M token. + * @param extensionIn The address of the $M Extension to swap from. + * @param amount The amount of $M Extension tokens to swap. + * @param recipient The address to receive $M tokens. + */ + function swapOutM(address extensionIn, uint256 amount, address recipient) external; + + /* ============ View/Pure Functions ============ */ + + /** + * @notice Returns the address that called `swap` or `swapM` + * @dev Must be used instead of `msg.sender` in $M Extensions contracts to get the original sender. + * @return The address of the original message sender. + */ + function msgSender() external view returns (address); +} diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index f684adb..ba4700a 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { IERC20Extended } from "../../lib/common/src/interfaces/IERC20Extended.sol"; - -import { IMigratable } from "./IMigratable.sol"; +import { IMigratable } from "../../lib/common/src/interfaces/IMigratable.sol"; /** * @title Wrapped M Token interface extending Extended ERC20. - * @author M^0 Labs + * @author M0 Labs */ interface IWrappedMToken is IMigratable, IERC20Extended { /* ============ Events ============ */ @@ -22,23 +21,36 @@ interface IWrappedMToken is IMigratable, IERC20Extended { event Claimed(address indexed account, address indexed recipient, uint240 yield); /** - * @notice Emitted when earning is enabled for the entire wrapper. - * @param index The index at the moment earning is enabled. + * @notice Emitted when `account` set their yield claim recipient. + * @param account The account that set their yield claim recipient. + * @param claimRecipient The account that will receive the yield. + */ + event ClaimRecipientSet(address indexed account, address indexed claimRecipient); + + /** + * @notice Emitted when Wrapped M earning is enabled. + * @param index The M index at the moment earning is enabled. */ event EarningEnabled(uint128 index); /** - * @notice Emitted when earning is disabled for the entire wrapper. - * @param index The index at the moment earning is disabled. + * @notice Emitted when Wrapped M earning is disabled. + * @param index The WrappedM index at the moment earning is disabled. */ event EarningDisabled(uint128 index); /** - * @notice Emitted when the wrapper's excess M is claimed. + * @notice Emitted when this contract's excess M is claimed. * @param excess The amount of excess M claimed. */ event ExcessClaimed(uint240 excess); + /** + * @notice Emitted when the excess destination is set. + * @param excessDestination The address of the new excess destination. + */ + event ExcessDestinationSet(address indexed excessDestination); + /** * @notice Emitted when `account` starts being an wM earner. * @param account The account that started earning. @@ -59,11 +71,11 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted when performing an operation that is not allowed when earning is enabled. error EarningIsEnabled(); - /// @notice Emitted when trying to enable earning after it has been explicitly disabled. - error EarningCannotBeReenabled(); - - /// @notice Emitted when calling `stopEarning` for an account approved as earner by TTG. - error IsApprovedEarner(); + /** + * @notice Emitted when calling `stopEarning` for an account approved as an earner. + * @param account The account that is an approved earner. + */ + error IsApprovedEarner(address account); /** * @notice Emitted when there is insufficient balance to decrement from `account`. @@ -73,49 +85,56 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ error InsufficientBalance(address account, uint240 balance, uint240 amount); - /// @notice Emitted when calling `startEarning` for an account not approved as earner by TTG. - error NotApprovedEarner(); + /** + * @notice Emitted when calling `startEarning` for an account not approved as an earner. + * @param account The account that is not an approved earner. + */ + error NotApprovedEarner(address account); - /// @notice Emitted when the non-governance migrate function is called by a account other than the migration admin. + /// @notice Emitted when the non-governance migrate function is called by an account other than the migration admin. error UnauthorizedMigration(); + /// @notice Emitted in constructor if default admin is 0x0. + error ZeroAdmin(); + + /// @notice Emitted in `initialize` if Excess Manager is 0x0. + error ZeroExcessManager(); + + /// @notice Emitted in `initialize` and `setExcessDestination` if Excess Destination is 0x0. + error ZeroExcessDestination(); + /// @notice Emitted in constructor if M Token is 0x0. error ZeroMToken(); /// @notice Emitted in constructor if Migration Admin is 0x0. error ZeroMigrationAdmin(); + /// @notice Emitted in constructor if Registrar is 0x0. + error ZeroRegistrar(); + + /// @notice Emitted in constructor if SwapFacility is 0x0. + error ZeroSwapFacility(); + + /// @notice Emitted in `wrap` and `unwrap` functions if the caller is not the SwapFacility. + error NotSwapFacility(); + /* ============ Interactive Functions ============ */ /** * @notice Wraps `amount` M from the caller into wM for `recipient`. + * @dev Can only be called by the SwapFacility. * @param recipient The account receiving the minted wM. - * @param amount The amount of M deposited. - * @return wrapped The amount of wM minted. + * @param amount The amount of wM minted. */ - function wrap(address recipient, uint256 amount) external returns (uint240 wrapped); + function wrap(address recipient, uint256 amount) external; /** - * @notice Wraps all the M from the caller into wM for `recipient`. - * @param recipient The account receiving the minted wM. - * @return wrapped The amount of wM minted. - */ - function wrap(address recipient) external returns (uint240 wrapped); - - /** - * @notice Unwraps `amount` wM from the caller into M for `recipient`. - * @param recipient The account receiving the withdrawn M. + * @notice Unwraps `amount` wM from the caller into M, sending the M to the SwapFacility. + * @dev Can only be called by the SwapFacility. + * @param recipient Unused. The M is always sent to the SwapFacility, which routes it to the final recipient. * @param amount The amount of wM burned. - * @return unwrapped The amount of M withdrawn. */ - function unwrap(address recipient, uint256 amount) external returns (uint240 unwrapped); - - /** - * @notice Unwraps all the wM from the caller into M for `recipient`. - * @param recipient The account receiving the withdrawn M. - * @return unwrapped The amount of M withdrawn. - */ - function unwrap(address recipient) external returns (uint240 unwrapped); + function unwrap(address recipient, uint256 amount) external; /** * @notice Claims any claimable yield for `account`. @@ -125,29 +144,54 @@ interface IWrappedMToken is IMigratable, IERC20Extended { function claimFor(address account) external returns (uint240 yield); /** - * @notice Claims any excess M of the wrapper. - * @return excess The amount of excess claimed. + * @notice Claims any excess M of this contract. + * @return claimed The amount of excess claimed. */ - function claimExcess() external returns (uint240 excess); + function claimExcess() external returns (uint240 claimed); - /// @notice Enables earning for the wrapper if allowed by TTG and if it has never been done. + /// @notice Enables earning of Wrapped M if allowed by the Registrar and not already enabled. function enableEarning() external; - /// @notice Disables earning for the wrapper if disallowed by TTG and if it has never been done. + /// @notice Disables earning of Wrapped M if disallowed by the Registrar and not already disabled. function disableEarning() external; /** - * @notice Starts earning for `account` if allowed by TTG. + * @notice Starts earning for `account` if allowed by the Registrar. * @param account The account to start earning for. */ function startEarningFor(address account) external; /** - * @notice Stops earning for `account` if disallowed by TTG. + * @notice Starts earning for multiple accounts if individually allowed by the Registrar. + * @param accounts The accounts to start earning for. + */ + function startEarningFor(address[] calldata accounts) external; + + /** + * @notice Stops earning for `account` if disallowed by the Registrar. * @param account The account to stop earning for. */ function stopEarningFor(address account) external; + /** + * @notice Stops earning for multiple accounts if individually disallowed by the Registrar. + * @param accounts The accounts to stop earning for. + */ + function stopEarningFor(address[] calldata accounts) external; + + /** + * @notice Explicitly sets the recipient of any yield claimed for the caller. + * @param claimRecipient The account that will receive the caller's yield. + */ + function setClaimRecipient(address claimRecipient) external; + + /** + * @notice Sets the destination where excess M is claimed to. + * @dev Can only be called by an account with the `EXCESS_MANAGER_ROLE`. + * @param excessDestination The address of the new excess destination. + */ + function setExcessDestination(address excessDestination) external; + /* ============ Temporary Admin Migration ============ */ /** @@ -158,6 +202,21 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /* ============ View/Pure Functions ============ */ + /// @notice The role that can set the excess destination. + function EXCESS_MANAGER_ROLE() external pure returns (bytes32 excessManagerRole); + + /// @notice Registrar key holding value of whether the earners list can be ignored or not. + function EARNERS_LIST_IGNORED_KEY() external pure returns (bytes32 earnersListIgnoredKey); + + /// @notice Registrar key of earners list. + function EARNERS_LIST_NAME() external pure returns (bytes32 earnersListName); + + /// @notice Registrar key prefix to determine the override recipient of an account's accrued yield. + function CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX() external pure returns (bytes32 claimOverrideRecipientKeyPrefix); + + /// @notice Registrar key prefix to determine the migrator contract. + function MIGRATOR_KEY_PREFIX() external pure returns (bytes32 migratorKeyPrefix); + /** * @notice Returns the yield accrued for `account`, which is claimable. * @param account The account being queried. @@ -173,45 +232,51 @@ interface IWrappedMToken is IMigratable, IERC20Extended { function balanceWithYieldOf(address account) external view returns (uint256 balance); /** - * @notice Returns the last index of `account`. - * @param account The address of some account. - * @return lastIndex The last index of `account`, 0 if the account is not earning. + * @notice Returns the earning principal of `account`. + * @param account The address of some account. + * @return earningPrincipal The earning principal of `account`. */ - function lastIndexOf(address account) external view returns (uint128 lastIndex); + function earningPrincipalOf(address account) external view returns (uint112 earningPrincipal); /** * @notice Returns the recipient to override as the destination for an account's claim of yield. * @param account The account being queried. * @return recipient The address of the recipient, if any, to override as the destination of claimed yield. */ - function claimOverrideRecipientFor(address account) external view returns (address recipient); + function claimRecipientFor(address account) external view returns (address recipient); - /// @notice The current index of the wrapper's earning mechanism. + /// @notice The current index of Wrapped M's earning mechanism. function currentIndex() external view returns (uint128 index); - /// @notice The current excess M of the wrapper that is not earmarked for account balances or accrued yield. - function excess() external view returns (uint240 excess); + /// @notice The M token's index when earning was most recently enabled. + function enableMIndex() external view returns (uint128 enableMIndex); + + /// @notice This contract's current excess M that is not earmarked for account balances or accrued yield. + function excess() external view returns (int256 excess); + + /// @notice The wrapper's index when earning was most recently disabled. + function disableIndex() external view returns (uint128 disableIndex); /** * @notice Returns whether `account` is a wM earner. * @param account The account being queried. - * @return isEarning true if the account has started earning. + * @return isEarning Whether the account is a wM earner. */ function isEarning(address account) external view returns (bool isEarning); - /// @notice Whether earning is enabled for the entire wrapper. + /// @notice Whether Wrapped M earning is enabled. function isEarningEnabled() external view returns (bool isEnabled); - /// @notice Whether earning has been enabled at least once or not. - function wasEarningEnabled() external view returns (bool wasEnabled); - - /// @notice The account that can bypass TTG and call the `migrate(address migrator)` function. + /// @notice The account that can bypass the Registrar and call the `migrate(address migrator)` function. function migrationAdmin() external view returns (address migrationAdmin); /// @notice The address of the M Token contract. function mToken() external view returns (address mToken); - /// @notice The address of the TTG registrar. + /// @notice The projected total earning supply if all accrued yield was claimed at this moment. + function projectedEarningSupply() external view returns (uint240 supply); + + /// @notice The address of the Registrar. function registrar() external view returns (address registrar); /// @notice The portion of total supply that is not earning yield. @@ -223,9 +288,12 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The portion of total supply that is earning yield. function totalEarningSupply() external view returns (uint240 totalSupply); - /// @notice The principal of totalEarningSupply to help compute totalAccruedYield(), and thus excess(). - function principalOfTotalEarningSupply() external view returns (uint112 principalOfTotalEarningSupply); + /// @notice The total earning principal to help compute totalAccruedYield(), and thus excess(). + function totalEarningPrincipal() external view returns (uint112 totalEarningPrincipal); + + /// @notice The address of the destination where excess is claimed to. + function excessDestination() external view returns (address excessDestination); - /// @notice The address of the vault where excess is claimed to. - function vault() external view returns (address vault); + /// @notice The address of the Swap Facility contract + function swapFacility() external view returns (address swapFacility); } diff --git a/src/libs/IndexingMath.sol b/src/libs/IndexingMath.sol deleted file mode 100644 index 121d42b..0000000 --- a/src/libs/IndexingMath.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.23; - -import { UIntMath } from "../../lib/common/src/libs/UIntMath.sol"; - -/** - * @title Helper library for indexing math functions. - * @author M^0 Labs - */ -library IndexingMath { - /* ============ Variables ============ */ - - /// @notice The scaling of indexes for exponent math. - uint56 internal constant EXP_SCALED_ONE = 1e12; - - /* ============ Custom Errors ============ */ - - /// @notice Emitted when a division by zero occurs. - error DivisionByZero(); - - /* ============ Internal View/Pure Functions ============ */ - - /** - * @notice Helper function to calculate `(x * EXP_SCALED_ONE) / y`, rounded down. - * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) - */ - function divide240By128Down(uint240 x_, uint128 y_) internal pure returns (uint112) { - if (y_ == 0) revert DivisionByZero(); - - unchecked { - // NOTE: While `uint256(x) * EXP_SCALED_ONE` can technically overflow, these divide/multiply functions are - // only used for the purpose of principal/present amount calculations for continuous indexing, and - // so for an `x` to be large enough to overflow this, it would have to be a possible result of - // `multiply112By128Down` or `multiply112By128Up`, which would already satisfy - // `uint256(x) * EXP_SCALED_ONE < type(uint240).max`. - return UIntMath.safe112((uint256(x_) * EXP_SCALED_ONE) / y_); - } - } - - /** - * @notice Helper function to calculate `(x * EXP_SCALED_ONE) / y`, rounded up. - * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) - */ - function divide240By128Up(uint240 x_, uint128 y_) internal pure returns (uint112) { - if (y_ == 0) revert DivisionByZero(); - - unchecked { - // NOTE: While `uint256(x) * EXP_SCALED_ONE` can technically overflow, these divide/multiply functions are - // only used for the purpose of principal/present amount calculations for continuous indexing, and - // so for an `x` to be large enough to overflow this, it would have to be a possible result of - // `multiply112By128Down` or `multiply112By128Up`, which would already satisfy - // `uint256(x) * EXP_SCALED_ONE < type(uint240).max`. - return UIntMath.safe112(((uint256(x_) * EXP_SCALED_ONE) + y_ - 1) / y_); - } - } - - /** - * @notice Helper function to calculate `(x * y) / EXP_SCALED_ONE`, rounded down. - * @dev Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) - */ - function multiply112By128Down(uint112 x_, uint128 y_) internal pure returns (uint240) { - unchecked { - return uint240((uint256(x_) * y_) / EXP_SCALED_ONE); - } - } - - /** - * @dev Returns the present amount (rounded down) given the principal amount and an index. - * @param principalAmount_ The principal amount. - * @param index_ An index. - * @return The present amount rounded down. - */ - function getPresentAmountRoundedDown(uint112 principalAmount_, uint128 index_) internal pure returns (uint240) { - return multiply112By128Down(principalAmount_, index_); - } - - /** - * @dev Returns the principal amount given the present amount, using the current index. - * @param presentAmount_ The present amount. - * @param index_ An index. - * @return The principal amount rounded down. - */ - function getPrincipalAmountRoundedDown(uint240 presentAmount_, uint128 index_) internal pure returns (uint112) { - return divide240By128Down(presentAmount_, index_); - } - - /** - * @dev Returns the principal amount given the present amount, using the current index. - * @param presentAmount_ The present amount. - * @param index_ An index. - * @return The principal amount rounded up. - */ - function getPrincipalAmountRoundedUp(uint240 presentAmount_, uint128 index_) internal pure returns (uint112) { - return divide240By128Up(presentAmount_, index_); - } -} diff --git a/test.sh b/test.sh index 978332c..af2141d 100755 --- a/test.sh +++ b/test.sh @@ -34,7 +34,7 @@ if [ "$gas" = false ]; then gasReport="" else - gasReport="--gas-report" + gasReport="--gas-report --isolate" fi if [ -z "$test" ]; then diff --git a/test/Migration.t.sol b/test/Migration.t.sol deleted file mode 100644 index e0d4a7f..0000000 --- a/test/Migration.t.sol +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.23; - -import { Test } from "../lib/forge-std/src/Test.sol"; - -import { IWrappedMToken } from "../src/interfaces/IWrappedMToken.sol"; - -import { WrappedMToken } from "../src/WrappedMToken.sol"; -import { Proxy } from "../src/Proxy.sol"; - -import { MockM, MockRegistrar } from "./utils/Mocks.sol"; - -contract WrappedMTokenV2 { - function foo() external pure returns (uint256) { - return 1; - } -} - -contract WrappedMTokenMigratorV1 { - bytes32 private constant _IMPLEMENTATION_SLOT = - bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc); - - address public immutable implementationV2; - - constructor(address implementationV2_) { - implementationV2 = implementationV2_; - } - - fallback() external virtual { - bytes32 slot_ = _IMPLEMENTATION_SLOT; - address implementationV2_ = implementationV2; - - assembly { - sstore(slot_, implementationV2_) - } - } -} - -contract MigrationTests is Test { - uint56 internal constant _EXP_SCALED_ONE = 1e12; - - bytes32 internal constant _EARNERS_LIST = "earners"; - bytes32 internal constant _MIGRATOR_V1_PREFIX = "wm_migrator_v1"; - - address internal _alice = makeAddr("alice"); - address internal _bob = makeAddr("bob"); - address internal _carol = makeAddr("carol"); - address internal _dave = makeAddr("dave"); - - address internal _migrationAdmin = makeAddr("migrationAdmin"); - - address internal _vault = makeAddr("vault"); - - MockM internal _mToken; - MockRegistrar internal _registrar; - WrappedMToken internal _implementation; - IWrappedMToken internal _wrappedMToken; - - function setUp() external { - _registrar = new MockRegistrar(); - _registrar.setVault(_vault); - - _mToken = new MockM(); - _mToken.setCurrentIndex(_EXP_SCALED_ONE); - _mToken.setTtgRegistrar(address(_registrar)); - - _implementation = new WrappedMToken(address(_mToken), _migrationAdmin); - - _wrappedMToken = IWrappedMToken(address(new Proxy(address(_implementation)))); - } - - function test_migration() external { - WrappedMTokenV2 implementationV2_ = new WrappedMTokenV2(); - address migrator_ = address(new WrappedMTokenMigratorV1(address(implementationV2_))); - - _registrar.set( - keccak256(abi.encode(_MIGRATOR_V1_PREFIX, address(_wrappedMToken))), - bytes32(uint256(uint160(migrator_))) - ); - - vm.expectRevert(); - WrappedMTokenV2(address(_wrappedMToken)).foo(); - - _wrappedMToken.migrate(); - - assertEq(WrappedMTokenV2(address(_wrappedMToken)).foo(), 1); - } - - function test_migration_fromAdmin() external { - WrappedMTokenV2 implementationV2_ = new WrappedMTokenV2(); - address migrator_ = address(new WrappedMTokenMigratorV1(address(implementationV2_))); - - vm.expectRevert(); - WrappedMTokenV2(address(_wrappedMToken)).foo(); - - vm.prank(_migrationAdmin); - _wrappedMToken.migrate(migrator_); - - assertEq(WrappedMTokenV2(address(_wrappedMToken)).foo(), 1); - } -} diff --git a/test/WrappedMToken.t.sol b/test/WrappedMToken.t.sol deleted file mode 100644 index 54fe412..0000000 --- a/test/WrappedMToken.t.sol +++ /dev/null @@ -1,1246 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity 0.8.23; - -import { Test, console2 } from "../lib/forge-std/src/Test.sol"; -import { IERC20Extended } from "../lib/common/src/interfaces/IERC20Extended.sol"; -import { UIntMath } from "../lib/common/src/libs/UIntMath.sol"; - -import { IWrappedMToken } from "../src/interfaces/IWrappedMToken.sol"; - -import { IndexingMath } from "../src/libs/IndexingMath.sol"; - -import { Proxy } from "../src/Proxy.sol"; - -import { MockM, MockRegistrar } from "./utils/Mocks.sol"; -import { WrappedMTokenHarness } from "./utils/WrappedMTokenHarness.sol"; - -// TODO: Test for `totalAccruedYield()`. -// TODO: All operations involving earners should include demonstration of accrued yield being added t their balance. -// TODO: Add relevant unit tests while earning enabled/disabled. - -contract WrappedMTokenTests is Test { - uint56 internal constant _EXP_SCALED_ONE = 1e12; - - bytes32 internal constant _EARNERS_LIST = "earners"; - bytes32 internal constant _CLAIM_DESTINATION_PREFIX = "wm_claim_destination"; - bytes32 internal constant _MIGRATOR_V1_PREFIX = "wm_migrator_v1"; - - address internal _alice = makeAddr("alice"); - address internal _bob = makeAddr("bob"); - address internal _charlie = makeAddr("charlie"); - address internal _david = makeAddr("david"); - - address internal _migrationAdmin = makeAddr("migrationAdmin"); - - address[] internal _accounts = [_alice, _bob, _charlie, _david]; - - address internal _vault = makeAddr("vault"); - - uint128 internal _currentIndex; - - MockM internal _mToken; - MockRegistrar internal _registrar; - WrappedMTokenHarness internal _implementation; - WrappedMTokenHarness internal _wrappedMToken; - - function setUp() external { - _registrar = new MockRegistrar(); - _registrar.setVault(_vault); - - _mToken = new MockM(); - _mToken.setCurrentIndex(_EXP_SCALED_ONE); - _mToken.setTtgRegistrar(address(_registrar)); - - _implementation = new WrappedMTokenHarness(address(_mToken), _migrationAdmin); - - _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); - - _mToken.setCurrentIndex(_currentIndex = 1_100000068703); - } - - /* ============ constructor ============ */ - function test_constructor() external view { - assertEq(_wrappedMToken.migrationAdmin(), _migrationAdmin); - assertEq(_wrappedMToken.mToken(), address(_mToken)); - assertEq(_wrappedMToken.registrar(), address(_registrar)); - assertEq(_wrappedMToken.vault(), _vault); - assertEq(_wrappedMToken.name(), "WrappedM by M^0"); - assertEq(_wrappedMToken.symbol(), "wM"); - assertEq(_wrappedMToken.decimals(), 6); - assertEq(_wrappedMToken.implementation(), address(_implementation)); - } - - function test_constructor_zeroMToken() external { - vm.expectRevert(IWrappedMToken.ZeroMToken.selector); - new WrappedMTokenHarness(address(0), address(0)); - } - - function test_constructor_zeroMigrationAdmin() external { - vm.expectRevert(IWrappedMToken.ZeroMigrationAdmin.selector); - new WrappedMTokenHarness(address(_mToken), address(0)); - } - - function test_constructor_zeroImplementation() external { - vm.expectRevert(); - WrappedMTokenHarness(address(new Proxy(address(0)))); - } - - /* ============ wrap ============ */ - function test_wrap_insufficientAmount() external { - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); - - _wrappedMToken.wrap(_alice, 0); - } - - function test_wrap_invalidRecipient() external { - _mToken.setBalanceOf(_alice, 1_000); - - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); - - vm.prank(_alice); - _wrappedMToken.wrap(address(0), 1_000); - } - - function test_wrap_invalidAmount() external { - _mToken.setBalanceOf(_alice, uint256(type(uint240).max) + 1); - - vm.expectRevert(UIntMath.InvalidUInt240.selector); - - vm.prank(_alice); - _wrappedMToken.wrap(_alice, uint256(type(uint240).max) + 1); - } - - function test_wrap_toNonEarner() external { - _mToken.setBalanceOf(_alice, 1_000); - - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 1_000), 1_000); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); - } - - function test_wrap_toEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); - - _mToken.setBalanceOf(_alice, 1_002); - - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 999), 999); - - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 909); - assertEq(_wrappedMToken.totalEarningSupply(), 999); - - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 1), 1); - - // No change due to principal round down on wrap. - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 910); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000); - - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 2), 2); - - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 1_002); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 912); - assertEq(_wrappedMToken.totalEarningSupply(), 1_002); - } - - function testFuzz_wrap( - bool earningEnabled_, - bool accountEarning_, - uint240 balance_, - uint240 wrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ - ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } - - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(currentIndex_) - balance_)); - - _mToken.setCurrentIndex(_currentIndex = currentIndex_); - _mToken.setBalanceOf(_alice, wrapAmount_); - - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - - if (wrapAmount_ == 0) { - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); - } - - vm.startPrank(_alice); - _wrappedMToken.wrap(_alice, wrapAmount_); - - if (wrapAmount_ == 0) return; - - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ + wrapAmount_); - - assertEq( - accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), - _wrappedMToken.balanceOf(_alice) - ); - } - - function testFuzz_wrapFull( - bool earningEnabled_, - bool accountEarning_, - uint240 balance_, - uint240 wrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ - ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } - - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(currentIndex_) - balance_)); - - _mToken.setCurrentIndex(_currentIndex = currentIndex_); - _mToken.setBalanceOf(_alice, wrapAmount_); - - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - - if (wrapAmount_ == 0) { - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); - } - - vm.startPrank(_alice); - _wrappedMToken.wrap(_alice); - - if (wrapAmount_ == 0) return; - - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ + wrapAmount_); - - assertEq( - accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), - _wrappedMToken.balanceOf(_alice) - ); - } - - /* ============ unwrap ============ */ - function test_unwrap_insufficientAmount() external { - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); - - _wrappedMToken.unwrap(_alice, 0); - } - - function test_unwrap_insufficientBalance_fromNonEarner() external { - _wrappedMToken.setAccountOf(_alice, 999); - - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); - vm.prank(_alice); - _wrappedMToken.unwrap(_alice, 1_000); - } - - function test_unwrap_insufficientBalance_fromEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); - - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); - vm.prank(_alice); - _wrappedMToken.unwrap(_alice, 1_000); - } - - function test_unwrap_fromNonEarner() external { - _wrappedMToken.setTotalNonEarningSupply(1_000); - - _wrappedMToken.setAccountOf(_alice, 1_000); - - _mToken.setBalanceOf(address(_wrappedMToken), 1_000); - - vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 500), 500); - - assertEq(_wrappedMToken.balanceOf(_alice), 500); - assertEq(_wrappedMToken.totalNonEarningSupply(), 500); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); - - vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 500), 500); - - assertEq(_wrappedMToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); - } - - function test_unwrap_fromEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); - _wrappedMToken.setTotalEarningSupply(1_000); - - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); - - _mToken.setBalanceOf(address(_wrappedMToken), 1_000); - - vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 1), 0); - - // Change due to principal round up on unwrap. - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 999); - - vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 999), 998); - - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - } - - function testFuzz_unwrap( - bool earningEnabled_, - bool accountEarning_, - uint240 balance_, - uint240 unwrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ - ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } - - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - unwrapAmount_ = uint240(bound(unwrapAmount_, 0, 2 * balance_)); - - _mToken.setCurrentIndex(_currentIndex = currentIndex_); - - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - - _mToken.setBalanceOf(address(_wrappedMToken), balance_ + accruedYield_); - - if (unwrapAmount_ == 0) { - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); - } else if (unwrapAmount_ > balance_ + accruedYield_) { - vm.expectRevert( - abi.encodeWithSelector( - IWrappedMToken.InsufficientBalance.selector, - _alice, - balance_ + accruedYield_, - unwrapAmount_ - ) - ); - } - - vm.startPrank(_alice); - _wrappedMToken.unwrap(_alice, unwrapAmount_); - - if ((unwrapAmount_ == 0) || (unwrapAmount_ > balance_ + accruedYield_)) return; - - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ - unwrapAmount_); - - assertEq( - accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), - _wrappedMToken.balanceOf(_alice) - ); - } - - function testFuzz_unwrapFull( - bool earningEnabled_, - bool accountEarning_, - uint240 balance_, - uint128 accountIndex_, - uint128 currentIndex_ - ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } - - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - - _mToken.setCurrentIndex(_currentIndex = currentIndex_); - - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - - _mToken.setBalanceOf(address(_wrappedMToken), balance_ + accruedYield_); - - if (balance_ + accruedYield_ == 0) { - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); - } - - vm.startPrank(_alice); - _wrappedMToken.unwrap(_alice); - - if (balance_ + accruedYield_ == 0) return; - - assertEq(_wrappedMToken.balanceOf(_alice), 0); - - assertEq(accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), 0); - } - - /* ============ claimFor ============ */ - function test_claimFor_nonEarner() external { - _wrappedMToken.setAccountOf(_alice, 1_000); - - vm.prank(_alice); - assertEq(_wrappedMToken.claimFor(_alice), 0); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - } - - function test_claimFor_earner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - - assertEq(_wrappedMToken.claimFor(_alice), 100); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_100); - } - - function testFuzz_claimFor(uint240 balance_, uint128 accountIndex_, uint128 index_) external { - accountIndex_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - index_ = uint128(bound(index_, accountIndex_, 10 * _EXP_SCALED_ONE)); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - - _mToken.setCurrentIndex(index_); - - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - - assertEq(_wrappedMToken.claimFor(_alice), accruedYield_); - - assertEq(_wrappedMToken.totalEarningSupply(), _wrappedMToken.balanceOf(_alice)); - } - - /* ============ claimExcess ============ */ - function testFuzz_claimExcess( - uint128 index_, - uint240 totalNonEarningSupply_, - uint112 principalOfTotalEarningSupply_, - uint240 mBalance_ - ) external { - index_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - - totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, _getMaxAmount(index_))); - - uint240 totalEarningSupply_ = uint112(bound(principalOfTotalEarningSupply_, 0, _getMaxAmount(index_))); - - principalOfTotalEarningSupply_ = uint112(totalEarningSupply_ / index_); - - mBalance_ = uint240(bound(mBalance_, totalNonEarningSupply_ + totalEarningSupply_, type(uint240).max)); - - _mToken.setBalanceOf(address(_wrappedMToken), mBalance_); - _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); - _wrappedMToken.setPrincipalOfTotalEarningSupply(principalOfTotalEarningSupply_); - - _mToken.setCurrentIndex(index_); - - uint240 expectedExcess_ = _wrappedMToken.excess(); - - vm.expectCall(address(_mToken), abi.encodeCall(_mToken.transfer, (_wrappedMToken.vault(), expectedExcess_))); - - assertEq(_wrappedMToken.claimExcess(), expectedExcess_); - assertEq(_wrappedMToken.excess(), 0); - } - - /* ============ transfer ============ */ - function test_transfer_invalidRecipient() external { - _wrappedMToken.setAccountOf(_alice, 1_000); - - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); - - vm.prank(_alice); - _wrappedMToken.transfer(address(0), 1_000); - } - - function test_transfer_insufficientBalance_fromNonEarner_toNonEarner() external { - _wrappedMToken.setAccountOf(_alice, 999); - - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); - vm.prank(_alice); - _wrappedMToken.transfer(_bob, 1_000); - } - - function test_transfer_insufficientBalance_fromEarner_toNonEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); - - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); - vm.prank(_alice); - _wrappedMToken.transfer(_bob, 1_000); - } - - function test_transfer_fromNonEarner_toNonEarner() external { - _wrappedMToken.setTotalNonEarningSupply(1_500); - - _wrappedMToken.setAccountOf(_alice, 1_000); - _wrappedMToken.setAccountOf(_bob, 500); - - vm.prank(_alice); - _wrappedMToken.transfer(_bob, 500); - - assertEq(_wrappedMToken.balanceOf(_alice), 500); - - assertEq(_wrappedMToken.balanceOf(_bob), 1_000); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 1_500); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); - } - - function testFuzz_transfer_fromNonEarner_toNonEarner( - uint256 supply_, - uint256 aliceBalance_, - uint256 transferAmount_ - ) external { - supply_ = bound(supply_, 1, type(uint112).max); - aliceBalance_ = bound(aliceBalance_, 1, supply_); - transferAmount_ = bound(transferAmount_, 1, aliceBalance_); - uint256 bobBalance = supply_ - aliceBalance_; - - _wrappedMToken.setTotalNonEarningSupply(supply_); - - _wrappedMToken.setAccountOf(_alice, aliceBalance_); - _wrappedMToken.setAccountOf(_bob, bobBalance); - - vm.prank(_alice); - _wrappedMToken.transfer(_bob, transferAmount_); - - assertEq(_wrappedMToken.balanceOf(_alice), aliceBalance_ - transferAmount_); - assertEq(_wrappedMToken.balanceOf(_bob), bobBalance + transferAmount_); - - assertEq(_wrappedMToken.totalNonEarningSupply(), supply_); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); - } - - function test_transfer_fromEarner_toNonEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); - _wrappedMToken.setTotalEarningSupply(1_000); - - _wrappedMToken.setTotalNonEarningSupply(500); - - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); - _wrappedMToken.setAccountOf(_bob, 500); - - vm.prank(_alice); - _wrappedMToken.transfer(_bob, 500); - - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 500); - - assertEq(_wrappedMToken.balanceOf(_bob), 1_000); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalEarningSupply(), 500); - - vm.prank(_alice); - _wrappedMToken.transfer(_bob, 1); - - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 499); - - assertEq(_wrappedMToken.balanceOf(_bob), 1_001); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 1_001); - assertEq(_wrappedMToken.totalEarningSupply(), 499); - } - - function test_transfer_fromNonEarner_toEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(454); - _wrappedMToken.setTotalEarningSupply(500); - - _wrappedMToken.setTotalNonEarningSupply(1_000); - - _wrappedMToken.setAccountOf(_alice, 1_000); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex); - - vm.prank(_alice); - _wrappedMToken.transfer(_bob, 500); - - assertEq(_wrappedMToken.balanceOf(_alice), 500); - - assertEq(_wrappedMToken.lastIndexOf(_bob), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_bob), 1_000); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 500); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000); - } - - function test_transfer_fromEarner_toEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(1_363); - _wrappedMToken.setTotalEarningSupply(1_500); - - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex); - - vm.prank(_alice); - _wrappedMToken.transfer(_bob, 500); - - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 500); - - assertEq(_wrappedMToken.lastIndexOf(_bob), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_bob), 1_000); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 1_500); - } - - function test_transfer_nonEarnerToSelf() external { - _wrappedMToken.setTotalNonEarningSupply(1_000); - - _wrappedMToken.setAccountOf(_alice, 1_000); - - vm.prank(_alice); - _wrappedMToken.transfer(_alice, 500); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); - } - - function test_transfer_earnerToSelf() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); - _wrappedMToken.setTotalEarningSupply(1_000); - - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); - - _mToken.setCurrentIndex((_currentIndex * 5) / 3); // 1_833333447838 - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 666); - - vm.prank(_alice); - _wrappedMToken.transfer(_alice, 500); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_666); - } - - function testFuzz_transfer( - bool earningEnabled_, - bool aliceEarning_, - bool bobEarning_, - uint240 aliceBalance_, - uint240 bobBalance_, - uint128 aliceIndex_, - uint128 bobIndex_, - uint128 currentIndex_, - uint240 amount_ - ) external { - aliceEarning_ = earningEnabled_ && aliceEarning_; - bobEarning_ = earningEnabled_ && bobEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - aliceIndex_ = uint128(bound(aliceIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - aliceBalance_ = uint240(bound(aliceBalance_, 0, _getMaxAmount(aliceIndex_) / 4)); - - if (aliceEarning_) { - _wrappedMToken.setAccountOf(_alice, aliceBalance_, aliceIndex_); - _wrappedMToken.setTotalEarningSupply(aliceBalance_); - - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(aliceBalance_, aliceIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, aliceBalance_); - _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); - } - - bobIndex_ = uint128(bound(bobIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - bobBalance_ = uint240(bound(bobBalance_, 0, _getMaxAmount(bobIndex_) / 4)); - - if (bobEarning_) { - _wrappedMToken.setAccountOf(_bob, bobBalance_, bobIndex_); - _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + bobBalance_); - - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown( - _wrappedMToken.totalEarningSupply() + bobBalance_, - aliceIndex_ > bobIndex_ ? aliceIndex_ : bobIndex_ - ) - ); - } else { - _wrappedMToken.setAccountOf(_bob, bobBalance_); - _wrappedMToken.setTotalNonEarningSupply(_wrappedMToken.totalNonEarningSupply() + bobBalance_); - } - - currentIndex_ = uint128( - bound(currentIndex_, aliceIndex_ > bobIndex_ ? aliceIndex_ : bobIndex_, 10 * _EXP_SCALED_ONE) - ); - - _mToken.setCurrentIndex(_currentIndex = currentIndex_); - - uint240 aliceAccruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - uint240 bobAccruedYield_ = _wrappedMToken.accruedYieldOf(_bob); - - amount_ = uint240(bound(amount_, 0, aliceBalance_ + aliceAccruedYield_)); - - if (amount_ > aliceBalance_ + aliceAccruedYield_) { - vm.expectRevert( - abi.encodeWithSelector( - IWrappedMToken.InsufficientBalance.selector, - _alice, - aliceBalance_ + aliceAccruedYield_, - amount_ - ) - ); - } - - vm.prank(_alice); - _wrappedMToken.transfer(_bob, amount_); - - if (amount_ > aliceBalance_ + aliceAccruedYield_) return; - - assertEq(_wrappedMToken.balanceOf(_alice), aliceBalance_ + aliceAccruedYield_ - amount_); - assertEq(_wrappedMToken.balanceOf(_bob), bobBalance_ + bobAccruedYield_ + amount_); - - if (aliceEarning_ && bobEarning_) { - assertEq( - _wrappedMToken.totalEarningSupply(), - aliceBalance_ + aliceAccruedYield_ + bobBalance_ + bobAccruedYield_ - ); - } else if (aliceEarning_) { - assertEq(_wrappedMToken.totalEarningSupply(), aliceBalance_ + aliceAccruedYield_ - amount_); - assertEq(_wrappedMToken.totalNonEarningSupply(), bobBalance_ + bobAccruedYield_ + amount_); - } else if (bobEarning_) { - assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ + aliceAccruedYield_ - amount_); - assertEq(_wrappedMToken.totalEarningSupply(), bobBalance_ + bobAccruedYield_ + amount_); - } else { - assertEq( - _wrappedMToken.totalNonEarningSupply(), - aliceBalance_ + aliceAccruedYield_ + bobBalance_ + bobAccruedYield_ - ); - } - } - - /* ============ startEarningFor ============ */ - function test_startEarningFor_notApprovedEarner() external { - vm.expectRevert(IWrappedMToken.NotApprovedEarner.selector); - _wrappedMToken.startEarningFor(_alice); - } - - function test_startEarningFor_earningIsDisabled() external { - _registrar.setListContains(_EARNERS_LIST, _alice, true); - - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.startEarningFor(_alice); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); - - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.startEarningFor(_alice); - } - - function test_startEarningFor() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setTotalNonEarningSupply(1_000); - - _wrappedMToken.setAccountOf(_alice, 1_000); - - _registrar.setListContains(_EARNERS_LIST, _alice, true); - - vm.expectEmit(); - emit IWrappedMToken.StartedEarning(_alice); - - _wrappedMToken.startEarningFor(_alice); - - assertEq(_wrappedMToken.isEarning(_alice), true); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 1000); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000); - } - - function test_startEarning_overflow() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - uint256 aliceBalance_ = uint256(type(uint112).max) + 20; - - _mToken.setCurrentIndex(_currentIndex = _EXP_SCALED_ONE); - - _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); - - _wrappedMToken.setAccountOf(_alice, aliceBalance_); - - _registrar.setListContains(_EARNERS_LIST, _alice, true); - - vm.expectRevert(UIntMath.InvalidUInt112.selector); - _wrappedMToken.startEarningFor(_alice); - } - - function testFuzz_startEarningFor(uint240 balance_, uint128 index_) external { - balance_ = uint240(bound(balance_, 0, _getMaxAmount(_currentIndex))); - index_ = uint128(bound(index_, _currentIndex, 10 * _EXP_SCALED_ONE)); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setTotalNonEarningSupply(balance_); - - _wrappedMToken.setAccountOf(_alice, balance_); - - _registrar.setListContains(_EARNERS_LIST, _alice, true); - - _mToken.setCurrentIndex(index_); - - _wrappedMToken.startEarningFor(_alice); - - assertEq(_wrappedMToken.isEarning(_alice), true); - assertEq(_wrappedMToken.lastIndexOf(_alice), index_); - assertEq(_wrappedMToken.balanceOf(_alice), balance_); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), balance_); - } - - /* ============ stopEarningFor ============ */ - function test_stopEarningFor_isApprovedEarner() external { - _registrar.setListContains(_EARNERS_LIST, _alice, true); - - vm.expectRevert(IWrappedMToken.IsApprovedEarner.selector); - _wrappedMToken.stopEarningFor(_alice); - } - - function test_stopEarningFor() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); - _wrappedMToken.setTotalEarningSupply(1_000); - - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); - - vm.expectEmit(); - emit IWrappedMToken.StoppedEarning(_alice); - - _wrappedMToken.stopEarningFor(_alice); - - assertEq(_wrappedMToken.balanceOf(_alice), 999); - assertEq(_wrappedMToken.isEarning(_alice), false); - - assertEq(_wrappedMToken.totalNonEarningSupply(), 999); - assertEq(_wrappedMToken.totalEarningSupply(), 1); - } - - function testFuzz_stopEarningFor(uint240 balance_, uint128 accountIndex_, uint128 index_) external { - accountIndex_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - index_ = uint128(bound(index_, accountIndex_, 10 * _EXP_SCALED_ONE)); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - - _mToken.setCurrentIndex(index_); - - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - - _wrappedMToken.stopEarningFor(_alice); - - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_); - assertEq(_wrappedMToken.isEarning(_alice), false); - - assertEq(_wrappedMToken.totalNonEarningSupply(), balance_ + accruedYield_); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - } - - /* ============ enableEarning ============ */ - function test_enableEarning_notApprovedEarner() external { - vm.expectRevert(IWrappedMToken.NotApprovedEarner.selector); - _wrappedMToken.enableEarning(); - } - - function test_enableEarning_earningCannotBeReenabled() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - vm.expectRevert(IWrappedMToken.EarningCannotBeReenabled.selector); - _wrappedMToken.enableEarning(); - } - - function test_enableEarning() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - vm.expectEmit(); - emit IWrappedMToken.EarningEnabled(_currentIndex); - - _wrappedMToken.enableEarning(); - } - - /* ============ disableEarning ============ */ - function test_disableEarning_earningIsDisabled() external { - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.disableEarning(); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); - - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.disableEarning(); - } - - function test_disableEarning_approvedEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - vm.expectRevert(IWrappedMToken.IsApprovedEarner.selector); - _wrappedMToken.disableEarning(); - } - - function test_disableEarning() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); - - vm.expectEmit(); - emit IWrappedMToken.EarningDisabled(_currentIndex); - - _wrappedMToken.disableEarning(); - } - - /* ============ balanceOf ============ */ - function test_balanceOf_nonEarner() external { - _wrappedMToken.setAccountOf(_alice, 500); - - assertEq(_wrappedMToken.balanceOf(_alice), 500); - - _wrappedMToken.setAccountOf(_alice, 1_000); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - } - - function test_balanceOf_earner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); - - assertEq(_wrappedMToken.balanceOf(_alice), 500); - - _wrappedMToken.setAccountOf(_alice, 1_000); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - - _wrappedMToken.setLastIndexOf(_alice, 2 * _EXP_SCALED_ONE); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - } - - /* ============ totalSupply ============ */ - function test_totalSupply_onlyTotalNonEarningSupply() external { - _wrappedMToken.setTotalNonEarningSupply(500); - - assertEq(_wrappedMToken.totalSupply(), 500); - - _wrappedMToken.setTotalNonEarningSupply(1_000); - - assertEq(_wrappedMToken.totalSupply(), 1_000); - } - - function test_totalSupply_onlyTotalEarningSupply() external { - _wrappedMToken.setTotalEarningSupply(500); - - assertEq(_wrappedMToken.totalSupply(), 500); - - _wrappedMToken.setTotalEarningSupply(1_000); - - assertEq(_wrappedMToken.totalSupply(), 1_000); - } - - function test_totalSupply() external { - _wrappedMToken.setTotalEarningSupply(400); - - _wrappedMToken.setTotalNonEarningSupply(600); - - assertEq(_wrappedMToken.totalSupply(), 1_000); - - _wrappedMToken.setTotalEarningSupply(700); - - assertEq(_wrappedMToken.totalSupply(), 1_300); - - _wrappedMToken.setTotalNonEarningSupply(1_000); - - assertEq(_wrappedMToken.totalSupply(), 1_700); - } - - /* ============ currentIndex ============ */ - function test_currentIndex() external { - assertEq(_wrappedMToken.currentIndex(), 0); - - _mToken.setCurrentIndex(2 * _EXP_SCALED_ONE); - - assertEq(_wrappedMToken.currentIndex(), 0); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - assertEq(_wrappedMToken.currentIndex(), 2 * _EXP_SCALED_ONE); - - _mToken.setCurrentIndex(3 * _EXP_SCALED_ONE); - - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); - - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); - - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); - - _mToken.setCurrentIndex(4 * _EXP_SCALED_ONE); - - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); - } - - /* ============ misc ============ */ - function testFuzz_wrap_transfer_unwrap( - bool aliceIsEarning_, - uint240 aliceWrap_, - bool bobIsEarning_, - uint240 bobWrap_, - uint240 transfer_, - uint128 index_ - ) external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); - _registrar.setListContains(_EARNERS_LIST, _alice, true); - _registrar.setListContains(_EARNERS_LIST, _bob, true); - _wrappedMToken.enableEarning(); - - _mToken.setCurrentIndex(index_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE))); - - aliceWrap_ = uint240(bound(aliceWrap_, 0, _getMaxAmount(index_) / 3)); - bobWrap_ = uint240(bound(bobWrap_, 0, _getMaxAmount(index_) / 3)); - - _mToken.setBalanceOf(_alice, aliceWrap_); - _mToken.setBalanceOf(_bob, bobWrap_); - - if (aliceIsEarning_) { - _wrappedMToken.startEarningFor(_alice); - } - - if (aliceWrap_ != 0) { - vm.prank(_alice); - _wrappedMToken.wrap(_alice, aliceWrap_); - } - - _mToken.setCurrentIndex(index_ = uint128(bound(index_, index_, 10 * _EXP_SCALED_ONE))); - - if (bobIsEarning_) { - _wrappedMToken.startEarningFor(_bob); - } - - if (bobWrap_ != 0) { - vm.prank(_bob); - _wrappedMToken.wrap(_bob, bobWrap_); - } - - _mToken.setCurrentIndex(index_ = uint128(bound(index_, index_, 10 * _EXP_SCALED_ONE))); - - uint240 aliceYield_ = _wrappedMToken.accruedYieldOf(_alice); - uint240 bobYield_ = _wrappedMToken.accruedYieldOf(_bob); - - transfer_ = uint240(bound(transfer_, 0, _wrappedMToken.balanceWithYieldOf(_alice))); - - _mToken.setCurrentIndex(index_ = uint128(bound(index_, index_, 10 * _EXP_SCALED_ONE))); - - aliceYield_ += _wrappedMToken.accruedYieldOf(_alice); - - if (_wrappedMToken.balanceWithYieldOf(_alice) != 0) { - vm.prank(_alice); - _wrappedMToken.unwrap(_charlie); - } - - _mToken.setCurrentIndex(index_ = uint128(bound(index_, index_, 10 * _EXP_SCALED_ONE))); - - bobYield_ += _wrappedMToken.accruedYieldOf(_bob); - - if (_wrappedMToken.balanceWithYieldOf(_bob) != 0) { - vm.prank(_bob); - _wrappedMToken.unwrap(_charlie); - } - - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - - uint240 total_ = aliceWrap_ + aliceYield_ + bobWrap_ + bobYield_; - - if (total_ < 100e6) { - assertApproxEqAbs(_mToken.balanceOf(_charlie), total_, 100); - } else { - assertApproxEqRel(_mToken.balanceOf(_charlie), total_, 1e12); - } - } - - /* ============ utils ============ */ - function _getPrincipalAmountRoundedDown(uint240 presentAmount_, uint128 index_) internal pure returns (uint112) { - return IndexingMath.divide240By128Down(presentAmount_, index_); - } - - function _getPresentAmountRoundedDown(uint112 principalAmount_, uint128 index_) internal pure returns (uint240) { - return IndexingMath.multiply112By128Down(principalAmount_, index_); - } - - function _getMaxAmount(uint128 index_) internal pure returns (uint240 maxAmount_) { - return (uint240(type(uint112).max) * index_) / _EXP_SCALED_ONE; - } -} diff --git a/test/harness/WrappedMTokenHarness.sol b/test/harness/WrappedMTokenHarness.sol new file mode 100644 index 0000000..fb370ee --- /dev/null +++ b/test/harness/WrappedMTokenHarness.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { WrappedMToken } from "../../src/WrappedMToken.sol"; + +contract WrappedMTokenHarness is WrappedMToken { + constructor( + address mToken_, + address registrar_, + address swapFacility_, + address migrationAdmin_ + ) WrappedMToken(mToken_, registrar_, swapFacility_, migrationAdmin_) {} + + function internalWrap(address account_, address recipient_, uint240 amount_) external { + _wrap(account_, recipient_, amount_); + } + + function internalUnwrap(address account_, uint240 amount_) external { + _unwrap(account_, amount_); + } + + function setIsEarningOf(address account_, bool isEarning_) external { + _accounts[account_].isEarning = isEarning_; + } + + function setEarningPrincipalOf(address account_, uint256 earningPrincipal_) external { + _accounts[account_].earningPrincipal = uint112(earningPrincipal_); + } + + function setAccountOf( + address account_, + uint256 balance_, + uint256 earningPrincipal_, + bool hasClaimRecipient_ + ) external { + _accounts[account_] = Account(true, uint240(balance_), uint112(earningPrincipal_), hasClaimRecipient_); + } + + function setAccountOf(address account_, uint256 balance_) external { + _accounts[account_] = Account(false, uint240(balance_), 0, false); + } + + function setInternalClaimRecipient(address account_, address claimRecipient_) external { + _claimRecipients[account_] = claimRecipient_; + } + + function setTotalNonEarningSupply(uint256 totalNonEarningSupply_) external { + totalNonEarningSupply = uint240(totalNonEarningSupply_); + } + + function setTotalEarningSupply(uint256 totalEarningSupply_) external { + totalEarningSupply = uint240(totalEarningSupply_); + } + + function setTotalEarningPrincipal(uint256 totalEarningPrincipal_) external { + totalEarningPrincipal = uint112(totalEarningPrincipal_); + } + + function setEnableMIndex(uint256 enableMIndex_) external { + enableMIndex = uint128(enableMIndex_); + } + + function setDisableIndex(uint256 disableIndex_) external { + disableIndex = uint128(disableIndex_); + } + + function getAccountOf( + address account_ + ) external view returns (bool isEarning_, uint240 balance_, uint112 earningPrincipal_, bool hasClaimRecipient_) { + Account storage account = _accounts[account_]; + return (account.isEarning, account.balance, account.earningPrincipal, account.hasClaimRecipient); + } + + function getInternalClaimRecipientOf(address account_) external view returns (address claimRecipient_) { + return _claimRecipients[account_]; + } +} diff --git a/test/integration/Deploy.t.sol b/test/integration/Deploy.t.sol index 79aebfc..b008ad6 100644 --- a/test/integration/Deploy.t.sol +++ b/test/integration/Deploy.t.sol @@ -1,45 +1,57 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { Test } from "../../lib/forge-std/src/Test.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { IMTokenLike } from "../../src/interfaces/IMTokenLike.sol"; -import { IRegistrarLike } from "../../src/interfaces/IRegistrarLike.sol"; import { DeployBase } from "../../script/DeployBase.sol"; -contract Deploy is Test, DeployBase { - address internal constant _TTG_VAULT = 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD; - +contract DeployTests is Test, DeployBase { + address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; - address internal constant _MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault + address internal constant _SWAP_FACILITY = 0xB6807116b3B1B321a390594e31ECD6e0076f6278; address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; - uint256 internal constant _DEPLOYER_PROXY_NONCE = 40; - address internal constant _EXPECTED_PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; + + uint64 internal constant _DEPLOYER_NONCE = 195; + + function setUp() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 23_170_985); + } function test_deploy() external { - // Set nonce to 1 before `_DEPLOYER_PROXY_NONCE` since implementation is deployed before proxy. - vm.setNonce(_DEPLOYER, uint64(_DEPLOYER_PROXY_NONCE) - 1); + vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); + + (address expectedWrappedMTokenImplementation_, address expectedWrappedMTokenProxy_) = mockDeploy( + _DEPLOYER, + _DEPLOYER_NONCE + ); vm.startPrank(_DEPLOYER); - (address implementation_, address proxy_) = deploy(_M_TOKEN, _MIGRATION_ADMIN); + (address wrappedMTokenImplementation_, address wrappedMTokenProxy_) = deploy( + _M_TOKEN, + _REGISTRAR, + _SWAP_FACILITY, + _WRAPPED_M_MIGRATION_ADMIN + ); vm.stopPrank(); // Wrapped M Token Implementation assertions - assertEq(implementation_, getExpectedWrappedMTokenImplementation(_DEPLOYER, 39)); - assertEq(IWrappedMToken(implementation_).migrationAdmin(), _MIGRATION_ADMIN); - assertEq(IWrappedMToken(implementation_).mToken(), _M_TOKEN); - assertEq(IWrappedMToken(implementation_).registrar(), IMTokenLike(_M_TOKEN).ttgRegistrar()); - assertEq(IWrappedMToken(implementation_).vault(), IRegistrarLike(IMTokenLike(_M_TOKEN).ttgRegistrar()).vault()); - - // // Wrapped M Token Proxy assertions - assertEq(proxy_, getExpectedWrappedMTokenProxy(_DEPLOYER, 39)); - assertEq(proxy_, _EXPECTED_PROXY); - assertEq(IWrappedMToken(proxy_).migrationAdmin(), _MIGRATION_ADMIN); - assertEq(IWrappedMToken(proxy_).mToken(), _M_TOKEN); - assertEq(IWrappedMToken(proxy_).registrar(), IMTokenLike(_M_TOKEN).ttgRegistrar()); - assertEq(IWrappedMToken(proxy_).vault(), IRegistrarLike(IMTokenLike(_M_TOKEN).ttgRegistrar()).vault()); + assertEq(wrappedMTokenImplementation_, expectedWrappedMTokenImplementation_); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).swapFacility(), _SWAP_FACILITY); + + // Wrapped M Token Proxy assertions + assertEq(wrappedMTokenProxy_, expectedWrappedMTokenProxy_); + assertEq(IWrappedMToken(wrappedMTokenProxy_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); + assertEq(IWrappedMToken(wrappedMTokenProxy_).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(wrappedMTokenProxy_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(wrappedMTokenProxy_).swapFacility(), _SWAP_FACILITY); + assertEq(IWrappedMToken(wrappedMTokenProxy_).implementation(), wrappedMTokenImplementation_); } } diff --git a/test/integration/Migration.t.sol b/test/integration/Migration.t.sol new file mode 100644 index 0000000..f18928b --- /dev/null +++ b/test/integration/Migration.t.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +import { console } from "../../lib/forge-std/src/Test.sol"; + +import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; + +import { TestBase } from "./TestBase.sol"; + +contract MigrationIntegrationTests is TestBase { + address[] internal _holders = [ + 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, + 0x9c6e67fA86138Ab49359F595BfE4Fb163D0f16cc, + 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, + 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, + 0xcAD001c30E96765aC90307669d578219D4fb1DCe, + 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, + 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, + 0xDeD796De6a14E255487191963dEe436c45995813, + 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90 + ]; + + function setUp() public override { + super.setUp(); + + vm.selectFork(mainnetFork); + } + + function test_initialState() external view { + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_418_521_333405); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_142_615762); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_093_596157); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_146361); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 43_084797); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 29_760_546197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_341_395374); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 33_760983); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 921_119567); + + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); + } + + function test_index_noMigration() external { + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_521_001_729306); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_233_405166); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_224_681607); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_449175); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 44_910436); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 31_021_594638); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_398_234537); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 35_191543); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 960_150316); + + assertEq(_wrappedMToken.currentIndex(), 1_099153050162); + } + + function test_index_migrate_earningNotDisabled() external { + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 0); + assertEq(_wrappedMToken.enableMIndex(), IndexingMath.EXP_SCALED_ONE); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_418_521_333405); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_142_615762); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_093_596157); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_146361); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 43_084797); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 29_760_546197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_341_395374); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 33_760983); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 921_119567); + + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_521_001_729306); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_233_405166); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_224_681607); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_449175); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 44_910436); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 31_021_594638); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_398_234537); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 35_191543); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 960_150316); + + assertEq(_wrappedMToken.currentIndex(), 1_099153050162); + } + + function test_index_migrate_earningDisabled_immediatelyReenabled() external { + _removeFromList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.disableEarning(); + + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); + assertEq(_wrappedMToken.enableMIndex(), 0); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_418_521_333405); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_142_615762); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_093_596157); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_146361); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 43_084797); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 29_760_546197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_341_395374); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 33_760983); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 921_119567); + + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); + + _addToList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.enableEarning(); + + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); + assertEq(_wrappedMToken.enableMIndex(), 1_054471748112); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_418_521_333405); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_142_615762); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_093_596157); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_146361); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 43_084797); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 29_760_546197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_341_395374); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 33_760983); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 921_119567); + + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_521_001_729306); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_233_405166); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_224_681607); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_449175); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 44_910436); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 31_021_594638); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_398_234537); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 35_191543); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 960_150316); + + assertEq(_wrappedMToken.currentIndex(), 1_099153050162); + } + + function test_index_migrate_earningDisabled_notReenabled() external { + _removeFromList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.disableEarning(); + + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); + assertEq(_wrappedMToken.enableMIndex(), 0); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_418_521_333405); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_142_615762); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_093_596157); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_146361); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 43_084797); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 29_760_546197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_341_395374); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 33_760983); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 921_119567); + + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_418_521_333405); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_142_615762); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_093_596157); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_146361); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 43_084797); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 29_760_546197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_341_395374); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 33_760983); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 921_119567); + + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); + } + + function test_index_migrate_earningDisabled_reenabledLater() external { + _removeFromList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.disableEarning(); + + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); + assertEq(_wrappedMToken.enableMIndex(), 0); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_418_521_333405); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_142_615762); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_093_596157); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_146361); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 43_084797); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 29_760_546197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_341_395374); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 33_760983); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 921_119567); + + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + _addToList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.enableEarning(); + + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); + assertEq(_wrappedMToken.enableMIndex(), 1_099153050162); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_418_521_333405); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_142_615762); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_093_596157); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_146361); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 43_084797); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 29_760_546197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_341_395374); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 33_760983); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 921_119567); + + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 2_521_001_729306 - 3); // Rounding error + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 2_233_405166); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 3_224_681607); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 7_449175); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 44_910436); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 31_021_594638); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 1_398_234537); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 35_191543); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 960_150316); + + assertEq(_wrappedMToken.currentIndex(), 1_099153050162 - 1); // Rounding error + } +} diff --git a/test/integration/MorphoBlue.t.sol b/test/integration/MorphoBlue.t.sol index 8f38a88..efdcb4e 100644 --- a/test/integration/MorphoBlue.t.sol +++ b/test/integration/MorphoBlue.t.sol @@ -1,30 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; - -import { TestBase } from "./TestBase.sol"; +pragma solidity 0.8.26; import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; -import { IMorphoBlueFactory, IMorphoChainlinkOracleV2Factory } from "./vendor/morpho-blue/Interfaces.sol"; - -contract MorphoBlueTests is TestBase { - // Morpho Blue factory on Ethereum Mainnet - address internal constant _morphoFactory = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb; - - // Oracle factory on Ethereum Mainnet - address internal constant _oracleFactory = 0x3A7bB36Ee3f3eE32A60e9f2b33c1e5f2E83ad766; - - // USDC on Ethereum Mainnet - address internal constant _USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - - // Morpho Blue market Liquidation Loan-To-Value ratio - uint256 internal constant _LLTV = 94_5000000000000000; // 94.5% - - address internal _oracle; - - uint256 internal _wrapperBalanceOfM; +import { MorphoTestBase } from "./vendor/morpho-blue/MorphoTestBase.sol"; +contract MorphoBlueTests is MorphoTestBase { uint256 internal _morphoBalanceOfUSDC; uint256 internal _aliceBalanceOfUSDC; uint256 internal _bobBalanceOfUSDC; @@ -32,639 +14,235 @@ contract MorphoBlueTests is TestBase { uint256 internal _daveBalanceOfUSDC; uint256 internal _morphoBalanceOfWM; + uint256 internal _morphoClaimRecipientBalanceOfWM; uint256 internal _aliceBalanceOfWM; uint256 internal _bobBalanceOfWM; uint256 internal _carolBalanceOfWM; uint256 internal _daveBalanceOfWM; uint256 internal _morphoAccruedYield; + uint256 internal _morphoClaimRecipientAccruedYield; + + uint240 internal _excess; function setUp() public override { super.setUp(); - _addToList(_EARNERS_LIST, address(_wrappedMToken)); + vm.selectFork(mainnetFork); - _wrappedMToken.enableEarning(); + _deployV2Components(); + _migrate(); _oracle = _createOracle(); - _morphoBalanceOfUSDC = IERC20(_USDC).balanceOf(_morphoFactory); + _morphoBalanceOfUSDC = IERC20(_USDC).balanceOf(_MORPHO); + _morphoBalanceOfWM = _wrappedMToken.balanceOf(_MORPHO); + _morphoAccruedYield = _wrappedMToken.accruedYieldOf(_MORPHO); } - function test_initialState() external view { + function test_state() external view { assertTrue(_mToken.isEarning(address(_wrappedMToken))); - assertEq(_wrappedMToken.isEarningEnabled(), true); - assertFalse(_wrappedMToken.isEarning(_morphoFactory)); - } - - function test_morphoBlue_nonEarning_wM_as_collateralToken() external { - /* ============ Alice Creates Market ============ */ - - _giveM(_alice, 1_000_100e6); - _wrap(_alice, _alice, 1_000_100e6); - - assertEq(_mToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_000_099_999999); - - deal(_USDC, _alice, 1_000_100e6); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_100e6); - - // NOTE: Creating a market also result in `_alice` supplying 1.00 USDC as supply, 1.00 wM as collateral, and - // borrowing 0.90 USDC. - _createMarket(_alice, _USDC, address(_wrappedMToken)); - - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 1e6); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 100000); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 100000); - - /* ============ Alice Supplies Seed USDC For Loans ============ */ - - _supply(_alice, _USDC, 1_000_000e6); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 1_000_000e6); - - /* ============ Bob Takes Out USDC Loan Against wM Collateral ============ */ - - _giveM(_bob, 1_000_100e6); - _wrap(_bob, _bob, 1_000_100e6); - - assertEq(_mToken.balanceOf(_bob), 0); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 1_000_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_000_099_999999); - - _supplyCollateral(_bob, address(_wrappedMToken), 1_000_000e6); - - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 1_000_000e6); - - _borrow(_bob, _USDC, 900_000e6, _bob); - - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 900_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC -= 900_000e6); - - /* ============ First 1-Year Time Warp ============ */ - - // Move 1 year forward and check that no yield has accrued. - vm.warp(vm.getBlockTimestamp() + 365 days); - - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 82_462_608992); - - // `startEarningFor` hasn't been called so no wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 82_462_608991); - - // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC); - - /* ============ Bob Repays USDC Loan And Withdraws wM Collateral ============ */ - - _repay(_bob, _USDC, 900_000e6); - - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 900_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 900_000e6); - - _withdrawCollateral(_bob, address(_wrappedMToken), 1_000_000e6, _bob); - - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM -= 1_000_000e6); - - /* ============ Alice Withdraws Seed USDC For Loans ============ */ - - _withdraw(_alice, _USDC, 1_000_000e6, _alice); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC -= 1_000_000e6); - - /* ============ Second 1-Year Time Warp ============ */ - - // Move 1 year forward and check that no yield has accrued. - vm.warp(vm.getBlockTimestamp() + 365 days); - - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 85_862_309962); - - // `startEarningFor` hasn't been called so no wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 168_324_918953); - - // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC); - } - - function test_morphoBlue_nonEarning_wM_as_loanToken() external { - /* ============ Alice Creates Market ============ */ - - _giveM(_alice, 1_000_100e6); - _wrap(_alice, _alice, 1_000_100e6); - - assertEq(_mToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_000_099_999999); - - deal(_USDC, _alice, 1_000_100e6); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_100e6); - - // NOTE: Creating a market also result in `_alice` supplying 1.00 wM as supply, 1.00 USDC as collateral, and - // borrowing 0.90 wM. - _createMarket(_alice, address(_wrappedMToken), _USDC); - - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 100000); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 100000); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 1e6); - - /* ============ Alice Supplies Seed wM For Loans ============ */ - - _supply(_alice, address(_wrappedMToken), 1_000_000e6); - - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 1_000_000e6); - - /* ============ Bob Takes Out wM Loan Against USDC Collateral ============ */ - - deal(_USDC, _bob, 1_000_000e6); - - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 1_000_000e6); - - _supplyCollateral(_bob, _USDC, 1_000_000e6); - - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 1_000_000e6); - - _borrow(_bob, address(_wrappedMToken), 900_000e6, _bob); - - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 900_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM -= 900_000e6); - - /* ============ First 1-Year Time Warp ============ */ - - // Move 1 year forward and check that no yield has accrued. - vm.warp(vm.getBlockTimestamp() + 365 days); - - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 51_276_223485); - - // `startEarningFor` hasn't been called so no wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 51_276_223483); - - // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC); - - /* ============ Bob Repays wM Loan And Withdraws USDC Collateral ============ */ - - _repay(_bob, address(_wrappedMToken), 900_000e6); - - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 900_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 900_000e6); - - _withdrawCollateral(_bob, _USDC, 1_000_000e6, _bob); - - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC -= 1_000_000e6); - - /* ============ Alice Withdraws Seed wM For Loans ============ */ - - _withdraw(_alice, address(_wrappedMToken), 1_000_000e6, _alice); - - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM -= 1_000_000e6); - - /* ============ Second 1-Year Time Warp ============ */ - - // Move 1 year forward and check that no yield has accrued. - vm.warp(vm.getBlockTimestamp() + 365 days); - - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 53_905_211681); - - // `startEarningFor` hasn't been called so no wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 105_181_435164); - - // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC); + assertTrue(_wrappedMToken.isEarningEnabled()); + assertTrue(_wrappedMToken.isEarning(_MORPHO)); } function test_morphoBlue_earning_wM_as_collateralToken() public { /* ============ Alice Creates Market ============ */ - _giveM(_alice, 1_000_100e6); - _wrap(_alice, _alice, 1_000_100e6); + _giveWM(_alice, 1_001e6); - assertEq(_mToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_000_099_999999); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_001e6); - deal(_USDC, _alice, 1_000_100e6); + _give(_USDC, _alice, 1_001e6); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_100e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_001e6); // NOTE: Creating a market also result in `_alice` supplying 1.00 USDC as supply, 1.00 wM as collateral, and // borrowing 0.90 USDC. - _createMarket(_alice, _USDC, address(_wrappedMToken)); + _createMarket(_alice, _USDC); + + // The market creation has triggered a wM transfer but the yield has not been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield); assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 1e6); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 1e6); assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 100000); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 100000); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC += 100000); /* ============ Alice Supplies Seed USDC For Loans ============ */ - _supply(_alice, _USDC, 1_000_000e6); + _supply(_alice, _USDC, 1_000e6, address(_wrappedMToken)); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC += 1_000e6); /* ============ Bob Takes Out USDC Loan Against wM Collateral ============ */ - _giveM(_bob, 1_000_100e6); - _wrap(_bob, _bob, 1_000_100e6); - - assertEq(_mToken.balanceOf(_bob), 0); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 1_000_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_000_099_999999); - - _supplyCollateral(_bob, address(_wrappedMToken), 1_000_000e6); - - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 1_000_000e6); - - _borrow(_bob, _USDC, 900_000e6, _bob); + _giveWM(_bob, 1_100e6); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 900_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC -= 900_000e6); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 1_100e6); - /* ============ Morpho Becomes An Earner ============ */ + _supplyCollateral(_bob, address(_wrappedMToken), 1_000e6, _USDC); - _setClaimOverrideRecipient(_morphoFactory, _carol); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 1_000e6); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 1_000e6); - _addToList(_EARNERS_LIST, _morphoFactory); - _wrappedMToken.startEarningFor(_morphoFactory); + _borrow(_bob, _USDC, 900e6, _bob, address(_wrappedMToken)); - // Check that the pool is earning wM. - assertTrue(_wrappedMToken.isEarning(_morphoFactory)); - - assertEq(_wrappedMToken.claimOverrideRecipientFor(_morphoFactory), _carol); - - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 900e6); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC -= 900e6); /* ============ First 1-Year Time Warp ============ */ // Move 1 year forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 365 days); - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 82_462_608992); - - // `startEarningFor` has been called so wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield += 41_227_223004); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 41_235_385986); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 42_718348); // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); /* ============ Bob Repays USDC Loan And Withdraws wM Collateral ============ */ - _repay(_bob, _USDC, 900_000e6); + _repay(_bob, _USDC, 900e6, address(_wrappedMToken)); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 900_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 900_000e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 900e6); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC += 900e6); - _withdrawCollateral(_bob, address(_wrappedMToken), 1_000_000e6, _bob); + _withdrawCollateral(_bob, address(_wrappedMToken), 1_000e6, _bob, _USDC); - // The collateral withdrawal has triggered a wM transfer and the yield has been claimed to carol for the pool. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _morphoAccruedYield); + // The collateral withdrawal has triggered a wM transfer but the yield has not been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield -= _morphoAccruedYield); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 1_000e6); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM -= 1_000e6); /* ============ Alice Withdraws Seed USDC For Loans ============ */ - _withdraw(_alice, _USDC, 1_000_000e6, _alice); + _withdraw(_alice, _USDC, 1_000e6, _alice, address(_wrappedMToken)); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC -= 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC -= 1_000e6); - // /* ============ Second 1-Year Time Warp ============ */ + /* ============ Second 1-Year Time Warp ============ */ // Move 1 year forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 365 days); - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 85_862_309962); - - // `startEarningFor` has been called so wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield += 41226); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 127_097_654719); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 2_155299); // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); } function test_morphoBlue_earning_wM_as_loanToken() public { /* ============ Alice Creates Market ============ */ - _giveM(_alice, 1_000_100e6); - _wrap(_alice, _alice, 1_000_100e6); + _giveWM(_alice, 1_001e6); - assertEq(_mToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_000_099_999999); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_001e6); - deal(_USDC, _alice, 1_000_100e6); + _give(_USDC, _alice, 1_001e6); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_100e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_001e6); // NOTE: Creating a market also result in `_alice` supplying 1.00 wM as supply, 1.00 USDC as collateral, and // borrowing 0.90 wM. - _createMarket(_alice, address(_wrappedMToken), _USDC); + _createMarket(_alice, address(_wrappedMToken)); + + // The market creation has triggered a wM transfer but the yield has not been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield); assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 100000); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 100000); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 100000); assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 1e6); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC += 1e6); /* ============ Alice Supplies Seed wM For Loans ============ */ - _supply(_alice, address(_wrappedMToken), 1_000_000e6); + _supply(_alice, address(_wrappedMToken), 1_000e6, _USDC); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 1_000_000e6); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000e6); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 1_000e6); /* ============ Bob Takes Out wM Loan Against USDC Collateral ============ */ - deal(_USDC, _bob, 1_000_000e6); + _give(_USDC, _bob, 1_100e6); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 1_100e6); - _supplyCollateral(_bob, _USDC, 1_000_000e6); + _supplyCollateral(_bob, _USDC, 1_000e6, address(_wrappedMToken)); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC += 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC += 1_000e6); - _borrow(_bob, address(_wrappedMToken), 900_000e6, _bob); + _borrow(_bob, address(_wrappedMToken), 900e6, _bob, _USDC); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 900_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM -= 900_000e6); - - /* ============ Morpho Becomes An Earner ============ */ - - _setClaimOverrideRecipient(_morphoFactory, _carol); - - _addToList(_EARNERS_LIST, _morphoFactory); - _wrappedMToken.startEarningFor(_morphoFactory); - - // Check that the pool is earning wM. - assertTrue(_wrappedMToken.isEarning(_morphoFactory)); - - assertEq(_wrappedMToken.claimOverrideRecipientFor(_morphoFactory), _carol); - - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 900e6); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM -= 900e6); /* ============ First 1-Year Time Warp ============ */ // Move 1 year forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 365 days); - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 51_276_223485); - // `startEarningFor` has been called so wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield += 5_127_114764); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 46_149_108719); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 4_544367); // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); /* ============ Bob Repays wM Loan And Withdraws USDC Collateral ============ */ - _repay(_bob, address(_wrappedMToken), 900_000e6); + _repay(_bob, address(_wrappedMToken), 900e6, _USDC); - // The repay has triggered a wM transfer and the yield has been claimed to carol for the pool. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _morphoAccruedYield); + // The repay has triggered a wM transfer but the yield has not been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= 1); // Rounding error - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 900_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM += 900_000e6); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield -= _morphoAccruedYield); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 900e6); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 900e6); - _withdrawCollateral(_bob, _USDC, 1_000_000e6, _bob); + _withdrawCollateral(_bob, _USDC, 1_000e6, _bob, address(_wrappedMToken)); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC -= 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC -= 1_000e6); /* ============ Alice Withdraws Seed wM For Loans ============ */ - _withdraw(_alice, address(_wrappedMToken), 1_000_000e6, _alice); + _withdraw(_alice, address(_wrappedMToken), 1_000e6, _alice, _USDC); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM -= 1_000_000e6); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000e6); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM -= 1_000e6); /* ============ Second 1-Year Time Warp ============ */ // Move 1 year forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 365 days); - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 53_905_211681); - // `startEarningFor` has been called so wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_morphoFactory), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_morphoFactory), _morphoAccruedYield += 5126); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 100_054_315272); + assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 499611); // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_morphoFactory), _morphoBalanceOfUSDC); + assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); } - function _createOracle() internal returns (address oracle_) { - return - IMorphoChainlinkOracleV2Factory(_oracleFactory).createMorphoChainlinkOracleV2( - address(0), - 1, - address(0), - address(0), - 6, - address(0), - 1, - address(0), - address(0), - 6, - bytes32(0) - ); - } - - function _createMarket(address account_, address loanToken_, address collateralToken_) internal { - IMorphoBlueFactory.MarketParams memory marketParams_ = IMorphoBlueFactory.MarketParams({ - loanToken: loanToken_, - collateralToken: collateralToken_, - oracle: _oracle, - irm: address(0), - lltv: _LLTV - }); + function _createMarket(address account_, address loanToken_) internal { + address collateralToken_ = loanToken_ == address(_wrappedMToken) ? _USDC : address(_wrappedMToken); - vm.prank(account_); - IMorphoBlueFactory(_morphoFactory).createMarket(marketParams_); + _createMarket(account_, loanToken_, collateralToken_, _oracle, _LLTV); - _supply(account_, loanToken_, 1_000000); + _supply(account_, loanToken_, 1_000000, collateralToken_); // NOTE: Put up arbitrarily more than necessary as collateral because Morpho contract seems to lack critical // getter to determine additional collateral needed for some additional borrow amount. - _supplyCollateral(account_, collateralToken_, 1_000000); - - _borrow(account_, loanToken_, 900000, account_); - } - - function _approve(address token_, address account_, address spender_, uint256 amount_) internal { - vm.prank(account_); - IERC20(token_).approve(spender_, amount_); - } - - function _transfer(address token_, address sender_, address recipient_, uint256 amount_) internal { - vm.prank(sender_); - IERC20(token_).transfer(recipient_, amount_); - } - - function _supplyCollateral(address account_, address collateralToken_, uint256 amount_) internal { - _approve(collateralToken_, account_, _morphoFactory, amount_); - - IMorphoBlueFactory.MarketParams memory marketParams_ = IMorphoBlueFactory.MarketParams({ - loanToken: collateralToken_ == address(_wrappedMToken) ? _USDC : address(_wrappedMToken), - collateralToken: collateralToken_, - oracle: _oracle, - irm: address(0), - lltv: _LLTV - }); - - vm.prank(account_); - IMorphoBlueFactory(_morphoFactory).supplyCollateral(marketParams_, amount_, account_, hex""); - } - - function _withdrawCollateral( - address account_, - address collateralToken_, - uint256 amount_, - address receiver_ - ) internal { - IMorphoBlueFactory.MarketParams memory marketParams_ = IMorphoBlueFactory.MarketParams({ - loanToken: collateralToken_ == address(_wrappedMToken) ? _USDC : address(_wrappedMToken), - collateralToken: collateralToken_, - oracle: _oracle, - irm: address(0), - lltv: _LLTV - }); - - vm.prank(account_); - IMorphoBlueFactory(_morphoFactory).withdrawCollateral(marketParams_, amount_, account_, receiver_); - } - - function _supply( - address account_, - address loanToken_, - uint256 amount_ - ) internal returns (uint256 assetsSupplied_, uint256 sharesSupplied_) { - _approve(loanToken_, account_, _morphoFactory, amount_); - - IMorphoBlueFactory.MarketParams memory marketParams_ = IMorphoBlueFactory.MarketParams({ - loanToken: loanToken_, - collateralToken: loanToken_ == address(_wrappedMToken) ? _USDC : address(_wrappedMToken), - oracle: _oracle, - irm: address(0), - lltv: _LLTV - }); - - vm.prank(account_); - return IMorphoBlueFactory(_morphoFactory).supply(marketParams_, amount_, 0, account_, hex""); - } - - function _withdraw( - address account_, - address loanToken_, - uint256 amount_, - address receiver_ - ) internal returns (uint256 assetsWithdrawn_, uint256 sharesWithdrawn_) { - IMorphoBlueFactory.MarketParams memory marketParams_ = IMorphoBlueFactory.MarketParams({ - loanToken: loanToken_, - collateralToken: loanToken_ == address(_wrappedMToken) ? _USDC : address(_wrappedMToken), - oracle: _oracle, - irm: address(0), - lltv: _LLTV - }); - - vm.prank(account_); - return IMorphoBlueFactory(_morphoFactory).withdraw(marketParams_, amount_, 0, account_, receiver_); - } - - function _borrow( - address account_, - address loanToken_, - uint256 amount_, - address receiver_ - ) internal returns (uint256 assetsBorrowed_, uint256 sharesBorrowed_) { - IMorphoBlueFactory.MarketParams memory marketParams_ = IMorphoBlueFactory.MarketParams({ - loanToken: loanToken_, - collateralToken: loanToken_ == address(_wrappedMToken) ? _USDC : address(_wrappedMToken), - oracle: _oracle, - irm: address(0), - lltv: _LLTV - }); - - vm.prank(account_); - return IMorphoBlueFactory(_morphoFactory).borrow(marketParams_, amount_, 0, account_, receiver_); - } + _supplyCollateral(account_, collateralToken_, 1_000000, loanToken_); - function _repay( - address account_, - address loanToken_, - uint256 amount_ - ) internal returns (uint256 assetsRepaid_, uint256 sharesRepaid_) { - _approve(loanToken_, account_, _morphoFactory, amount_); - - IMorphoBlueFactory.MarketParams memory marketParams_ = IMorphoBlueFactory.MarketParams({ - loanToken: loanToken_, - collateralToken: loanToken_ == address(_wrappedMToken) ? _USDC : address(_wrappedMToken), - oracle: _oracle, - irm: address(0), - lltv: _LLTV - }); - - vm.prank(account_); - return IMorphoBlueFactory(_morphoFactory).repay(marketParams_, amount_, 0, account_, hex""); + _borrow(account_, loanToken_, 900000, account_, collateralToken_); } } diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index 823ab4a..e725ee8 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -1,16 +1,19 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.23; +pragma solidity 0.8.26; -// import { console2 } from "../../lib/forge-std/src/Test.sol"; +import { console2 } from "../../lib/forge-std/src/Test.sol"; import { Invariants } from "../utils/Invariants.sol"; +import { ISwapFacilityLike } from "./vendor/protocol/Interfaces.sol"; + import { TestBase } from "./TestBase.sol"; contract ProtocolIntegrationTests is TestBase { uint256 internal _wrapperBalanceOfM; uint256 internal _totalEarningSupplyOfM; + uint256 internal _totalNonEarningSupplyOfM; uint256 internal _aliceBalance; uint256 internal _bobBalance; @@ -22,26 +25,62 @@ contract ProtocolIntegrationTests is TestBase { uint256 internal _carolAccruedYield; uint256 internal _daveAccruedYield; - uint256 internal _excess; + uint256 internal _totalEarningSupply; + uint256 internal _totalNonEarningSupply; + uint256 internal _totalSupply; + uint256 internal _totalAccruedYield; + + int256 internal _excess; function setUp() public override { super.setUp(); - _addToList(_EARNERS_LIST, address(_wrappedMToken)); - _addToList(_EARNERS_LIST, _alice); - _addToList(_EARNERS_LIST, _bob); + vm.selectFork(mainnetFork); - _wrappedMToken.enableEarning(); + _deployV2Components(); + _migrate(); + + _addToList(_EARNERS_LIST_NAME, _alice); + _addToList(_EARNERS_LIST_NAME, _bob); _wrappedMToken.startEarningFor(_alice); _wrappedMToken.startEarningFor(_bob); + vm.prank(_m0Deployer); + ISwapFacilityLike(_swapFacility).grantRole(_M_SWAPPER_ROLE, _alice); + + vm.prank(_m0Deployer); + ISwapFacilityLike(_swapFacility).grantRole(_M_SWAPPER_ROLE, _bob); + + vm.prank(_m0Deployer); + ISwapFacilityLike(_swapFacility).grantRole(_M_SWAPPER_ROLE, _carol); + + vm.prank(_m0Deployer); + ISwapFacilityLike(_swapFacility).grantRole(_M_SWAPPER_ROLE, _dave); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + _totalNonEarningSupplyOfM = _mToken.totalNonEarningSupply(); + + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + + _totalEarningSupply = _wrappedMToken.totalEarningSupply(); + _totalNonEarningSupply = _wrappedMToken.totalNonEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); + } + + function test_constants() external view { + assertEq(_wrappedMToken.EARNERS_LIST_IGNORED_KEY(), "earners_list_ignored"); + assertEq(_wrappedMToken.EARNERS_LIST_NAME(), "earners"); + assertEq(_wrappedMToken.CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX(), "wm_claim_override_recipient"); + assertEq(_wrappedMToken.MIGRATOR_KEY_PREFIX(), "wm_migrator_v2"); + assertEq(_wrappedMToken.name(), "M (Wrapped) by M0"); + assertEq(_wrappedMToken.symbol(), "wM"); + assertEq(_wrappedMToken.decimals(), 6); } - function test_initialState() external view { + function test_state() external view { assertEq(_mToken.currentIndex(), _wrappedMToken.currentIndex()); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), 0); assertTrue(_mToken.isEarning(address(_wrappedMToken))); } @@ -53,77 +92,79 @@ contract ProtocolIntegrationTests is TestBase { _wrap(_alice, _alice, 100_000000); // Assert M Token - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM = 99_999999); - assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 99_999999); + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 99_999999); + + // No change in earning supply since Alice, mSource and WrappedM are earners + assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM -= 1); // Rounding error // Assert Alice (Earner) - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = 99_999999); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = 100_000000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 99_999999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalSupply(), 99_999999); - assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); _giveM(_carol, 50_000000); + assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM -= (50_000000 + 1)); // Rounding error + assertEq(_mToken.totalNonEarningSupply(), _totalNonEarningSupplyOfM += 50_000000); + assertEq(_mToken.balanceOf(_carol), 50_000000); _wrap(_carol, _carol, 50_000000); assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 50_000000); assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 50_000000); + assertEq(_mToken.totalNonEarningSupply(), _totalNonEarningSupplyOfM -= 50_000000); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance = 50_000000); assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 99_999999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); - assertEq(_wrappedMToken.totalSupply(), 149_999999); - assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 50_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); // Fast forward 90 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 90 days); - assertEq(_mToken.currentIndex(), _wrappedMToken.currentIndex()); + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); - // Assert M Token - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_860762); - assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 1_860762); + assertEq(_mToken.currentIndex(), _wrappedMToken.currentIndex()); // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield = 1_240507); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 1_028540); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 99_999999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); - assertEq(_wrappedMToken.totalSupply(), 149_999999); - assertEq(_wrappedMToken.totalAccruedYield(), 1_240508); - assertEq(_wrappedMToken.excess(), _excess = 62_0253); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); _giveM(_bob, 200_000000); @@ -134,48 +175,50 @@ contract ProtocolIntegrationTests is TestBase { // Assert M Token assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 199_999999); - assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 199_999999); + + // No change in earning supply since Bob, mSource and WrappedM are earners + assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM -= 1); // Rounding error // Assert Bob (Earner) - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance = 199_999999); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance = 200_000000); assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 299_999998); - assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); - assertEq(_wrappedMToken.totalSupply(), 349_999998); - assertEq(_wrappedMToken.totalAccruedYield(), 1_240509); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 200_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); _giveM(_dave, 150_000000); assertEq(_mToken.balanceOf(_dave), 150_000000); + assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM -= 150_000000); + assertEq(_mToken.totalNonEarningSupply(), _totalNonEarningSupplyOfM += 150_000000); _wrap(_dave, _dave, 150_000000); // Assert M Token - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 150_000000); - assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 150_000000); + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 149_999999); + assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 149_999999); // Assert Dave (Non-Earner) assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance = 150_000000); assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 299_999998); - assertEq(_wrappedMToken.totalNonEarningSupply(), 200_000000); - assertEq(_wrappedMToken.totalSupply(), 499_999998); - assertEq(_wrappedMToken.totalAccruedYield(), 1_240509); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 150_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); assertEq(_wrappedMToken.claimFor(_alice), _aliceAccruedYield); @@ -186,36 +229,36 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += _aliceAccruedYield); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 1_240507); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 1_028540); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 301_240505); - assertEq(_wrappedMToken.totalNonEarningSupply(), 200_000000); - assertEq(_wrappedMToken.totalSupply(), 501_240505); - assertEq(_wrappedMToken.totalAccruedYield(), 2); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 1_028540); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1_028540); assertEq(_wrappedMToken.excess(), _excess); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); // Fast forward 180 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 180 days); - assertEq(_mToken.currentIndex(), _wrappedMToken.currentIndex()); + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); - // Assert M Token - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 12_528475); - assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 12_528475); + assertEq(_mToken.currentIndex(), _wrappedMToken.currentIndex()); // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_527372); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_088928); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield = 4_992808); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield = 4_135321); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -226,15 +269,12 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 301_240505); - assertEq(_wrappedMToken.totalNonEarningSupply(), 200_000000); - assertEq(_wrappedMToken.totalSupply(), 501_240505); - assertEq(_wrappedMToken.totalAccruedYield(), 7_520183); - assertEq(_wrappedMToken.excess(), _excess += 5_008294); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); } @@ -245,27 +285,42 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 99_999999); // Assert Alice (Earner) - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = 99_999999); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = 100_000000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); _giveM(_carol, 100_000000); _wrap(_carol, _carol, 100_000000); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 100_000000); + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += (100_000000 - 1)); // Rounding error // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance = 100_000000); assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess -= 2); + + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); + // Fast forward 180 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 180 days); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 4_992809); + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); + assertEq(_mToken.currentIndex(), _wrappedMToken.currentIndex()); // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield = 2_496404); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield = 2_067660); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -274,42 +329,41 @@ contract ProtocolIntegrationTests is TestBase { _giveM(_bob, 100_000000); _wrap(_bob, _bob, 100_000000); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 99_999999); + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += (100_000000 - 1)); // Rounding error // Assert Bob (Earner) - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance = 99_999999); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance = 100_000000); assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); _giveM(_dave, 100_000000); _wrap(_dave, _dave, 100_000000); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 99_999999); + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 100_000000); // Assert Dave (Non-Earner) - assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance = 99_999999); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance = 100_000000); assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Alice transfers all her tokens and only keeps her accrued yield. _transferWM(_alice, _carol, 100_000000); - // Assert Alice (Earner) - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = _aliceBalance + _aliceAccruedYield - 100_000000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= _aliceAccruedYield); + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _bobBalance - _aliceBalance); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + _aliceBalance); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 2); + assertEq(_wrappedMToken.excess(), _excess -= 3); // Assert Carol (Non-Earner) - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += 100_000000); + assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += _aliceBalance); assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); - // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 102_496402); - assertEq(_wrappedMToken.totalNonEarningSupply(), 299_999999); - assertEq(_wrappedMToken.totalSupply(), 402_496401); - assertEq(_wrappedMToken.totalAccruedYield(), 2); - assertEq(_wrappedMToken.excess(), _excess = 2_496402); + // Assert Alice (Earner) + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= _aliceBalance); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield); assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); _transferWM(_dave, _bob, 50_000000); @@ -323,30 +377,33 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 152_496402); - assertEq(_wrappedMToken.totalNonEarningSupply(), 249_999999); - assertEq(_wrappedMToken.totalSupply(), 402_496401); - assertEq(_wrappedMToken.totalAccruedYield(), 2); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 50_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 50_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); // Fast forward 180 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 180 days); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 10_110259); + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); + assertEq(_mToken.currentIndex(), _wrappedMToken.currentIndex()); // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 62320); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 42752); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 3_744606); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 3_101490); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -357,15 +414,12 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 152_496402); - assertEq(_wrappedMToken.totalNonEarningSupply(), 249_999999); - assertEq(_wrappedMToken.totalSupply(), 402_496401); - assertEq(_wrappedMToken.totalAccruedYield(), 3_806929); - assertEq(_wrappedMToken.excess(), _excess += 6_303332); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); } @@ -373,63 +427,91 @@ contract ProtocolIntegrationTests is TestBase { _giveM(_alice, 100_000000); _wrap(_alice, _alice, 100_000000); - assertGe(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += (_aliceBalance = 99_999999)); - _giveM(_carol, 100_000000); _wrap(_carol, _carol, 100_000000); - assertGe(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += (_carolBalance = 100_000000)); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 100_000000); + assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += 100_000000); + + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += (200_000000 - 2)); // Rounding error + + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess -= 2); + + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 180 days in the future to generate yield. vm.warp(vm.getBlockTimestamp() + 180 days); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_496404); - assertEq(_wrappedMToken.excess(), _excess += 2_496403); + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); _giveM(_bob, 100_000000); _wrap(_bob, _bob, 100_000000); - assertGe( - _mToken.balanceOf(address(_wrappedMToken)), - _wrapperBalanceOfM += (_bobBalance = 99_999999) + _aliceAccruedYield + _excess - ); - _giveM(_dave, 100_000000); _wrap(_dave, _dave, 100_000000); - assertGe(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += (_daveBalance = 99_999999)); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_067660); + + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance += 100_000000); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance += 100_000000); + + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += (200_000000 - 1)); // Rounding error + + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); + + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 90 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 90 days); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 1_271476); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_240507); - assertEq(_wrappedMToken.excess(), _excess += 2_511984); + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 1_049808); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_028540); // Stop earning for Alice - _removeFomList(_EARNERS_LIST, _alice); + _removeFromList(_EARNERS_LIST_NAME, _alice); _wrappedMToken.stopEarningFor(_alice); // Assert Alice (Non-Earner) // Yield of Alice is claimed when stopping earning - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += _aliceAccruedYield); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= _aliceAccruedYield); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 3_117468); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 3_117468); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 99_999999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 303_767878); - assertEq(_wrappedMToken.totalSupply(), 403_767877); - assertEq(_wrappedMToken.totalAccruedYield(), 1_240510); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _aliceBalance); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_117468 + 1); + assertEq(_wrappedMToken.excess(), _excess += 1); assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _aliceBalance + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); // Start earning for Carol - _addToList(_EARNERS_LIST, _carol); + _addToList(_EARNERS_LIST_NAME, _carol); _wrappedMToken.startEarningFor(_carol); @@ -438,150 +520,152 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 199_999999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 203_767878); - assertEq(_wrappedMToken.totalSupply(), 403_767877); - assertEq(_wrappedMToken.totalAccruedYield(), 1_240510); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _carolBalance); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _carolBalance); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _aliceBalance + _bobBalance + _bobAccruedYield + _carolBalance + _carolAccruedYield + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); // Fast forward 180 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 180 days); - // Assert Bob (Earner) - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 2_527372); - - // Assert Carol (Earner) - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); - assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield += 2_496403); + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); - // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 199_999999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 203_767878); - assertEq(_wrappedMToken.totalSupply(), 403_767877); - assertEq(_wrappedMToken.totalAccruedYield(), 6_264288); - assertEq(_wrappedMToken.excess(), _excess += 5_211900); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 2_088928); + assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield += 2_067660); assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _aliceBalance + _bobBalance + _bobAccruedYield + _carolBalance + _carolAccruedYield + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); _unwrap(_alice, _alice, _aliceBalance); - // Assert Alice (Non-Earner) - assertEq(_mToken.balanceOf(_alice), _aliceBalance - 1); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield); - // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 199_999999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 99_999999); - assertEq(_wrappedMToken.totalSupply(), 299_999998); - assertEq(_wrappedMToken.totalAccruedYield(), 6_264288); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _aliceBalance); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); + // Assert Alice (Non-Earner) + assertEq(_mToken.balanceOf(_alice), _aliceBalance); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= _aliceBalance); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _bobBalance + _bobAccruedYield + _carolBalance + _carolAccruedYield + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); - // Accrued yield of Bob is claimed when unwrapping - _unwrap(_bob, _bob, _bobBalance + _bobAccruedYield); - - // Assert Bob (Earner) - assertEq(_mToken.balanceOf(_bob), _bobBalance + _bobAccruedYield - 1); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= _bobAccruedYield); + // Accrued yield of Bob is not claimed when unwrapping + _unwrap(_bob, _bob, _bobBalance); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 100_000000); - assertEq(_wrappedMToken.totalNonEarningSupply(), 99_999999); - assertEq(_wrappedMToken.totalSupply(), 199_999999); - assertEq(_wrappedMToken.totalAccruedYield(), 2_496409); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _bobBalance); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); + + // Assert Bob (Earner) + assertEq(_mToken.balanceOf(_bob), _bobBalance); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= 100_000000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= 1); assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _carolBalance + _carolAccruedYield + _daveBalance + _excess + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); - // Accrued yield of Carol is claimed when unwrapping - _unwrap(_carol, _carol, _carolBalance + _carolAccruedYield); + // Accrued yield of Carol is not claimed when unwrapping + _unwrap(_carol, _carol, _carolBalance); + + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _carolBalance); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); // Assert Carol (Earner) - assertEq(_mToken.balanceOf(_carol), _carolBalance + _carolAccruedYield - 1); + assertEq(_mToken.balanceOf(_carol), _carolBalance); assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance -= _carolBalance); - assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield -= _carolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield); - // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 99_999999); - assertEq(_wrappedMToken.totalSupply(), 99_999999); - assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), _excess += 6); - - assertGe(_wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), _daveBalance + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); _unwrap(_dave, _dave, _daveBalance); + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); + // Assert Dave (Non-Earner) - assertEq(_mToken.balanceOf(_dave), _daveBalance - 1); + assertEq(_mToken.balanceOf(_dave), _daveBalance); assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance -= _daveBalance); assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); - // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalSupply(), 0); - assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), _excess += 1); - - assertGe(_wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); - uint256 vaultStartingBalance_ = _mToken.balanceOf(_vault); + uint256 vaultStartingBalance_ = _wrappedMToken.balanceOf(_excessDestination); - assertEq(_wrappedMToken.claimExcess(), _excess); - assertEq(_mToken.balanceOf(_vault), _excess + vaultStartingBalance_); + assertEq(_wrappedMToken.claimExcess(), uint256(_excess)); + assertEq(_wrappedMToken.balanceOf(_excessDestination), uint256(_excess) + vaultStartingBalance_); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalSupply(), 0); - assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), _excess -= _excess); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += uint256(_excess)); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), 0); - assertGe(_wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), 0); + assertGe(int256(_wrapperBalanceOfM), int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield)); } function testFuzz_full(uint256 seed_) external { vm.skip(true); + _wrappedMToken.claimExcess(); + for (uint256 index_; index_ < _accounts.length; ++index_) { _giveM(_accounts[index_], 100_000e6); } - for (uint256 index_; index_ < 1000; ++index_) { + for (uint256 index_; index_ < 1_000; ++index_) { + // assertTrue(Invariants.checkInvariant1(address(_wrappedMToken), _accounts), "Invariant 1 Failed."); + // assertTrue(Invariants.checkInvariant2(address(_wrappedMToken), _accounts), "Invariant 2 Failed."); + assertTrue(Invariants.checkInvariant3(address(_wrappedMToken), address(_mToken)), "Invariant 3 Failed."); + // assertTrue(Invariants.checkInvariant4(address(_wrappedMToken), _accounts), "Invariant 4 Failed."); + // console2.log("--------"); + // console2.log(""); uint256 timeDelta_ = (seed_ = _getNewSeed(seed_)) % 30 days; - vm.warp(vm.getBlockTimestamp() + timeDelta_); - - assertTrue(Invariants.checkInvariant1(address(_wrappedMToken), _accounts), "Invariant 1 Failed."); - assertTrue(Invariants.checkInvariant2(address(_wrappedMToken), _accounts), "Invariant 2 Failed."); - assertTrue(Invariants.checkInvariant4(address(_wrappedMToken), _accounts), "Invariant 4 Failed."); + // console2.log("Warping %s hours", timeDelta_ / 1 hours); - // NOTE: Skipping this as there is no trivial way to guarantee this invariant while meeting 1 and 2. - // assertTrue(Invariants.checkInvariant3(address(_wrappedMToken), address(_mToken)), "Invariant 3 Failed."); + vm.warp(vm.getBlockTimestamp() + timeDelta_); // console2.log(""); // console2.log("--------"); + + // assertTrue(Invariants.checkInvariant1(address(_wrappedMToken), _accounts), "Invariant 1 Failed."); + // assertTrue(Invariants.checkInvariant2(address(_wrappedMToken), _accounts), "Invariant 2 Failed."); + assertTrue(Invariants.checkInvariant3(address(_wrappedMToken), address(_mToken)), "Invariant 3 Failed."); + // assertTrue(Invariants.checkInvariant4(address(_wrappedMToken), _accounts), "Invariant 4 Failed."); + // console2.log("Wrapper has %s M", _mToken.balanceOf(address(_wrappedMToken))); address account1_ = _accounts[((seed_ = _getNewSeed(seed_)) % _accounts.length)]; @@ -618,7 +702,7 @@ contract ProtocolIntegrationTests is TestBase { if (amount_ >= account1BalanceOfM_) { // console2.log("%s wrapping all their M to %s", account1_, account2_); - _wrap(account1_, account2_); + _wrap(account1_, account2_, account1BalanceOfM_ - 1); // -1 to account for rounding up for earners. } else { // console2.log("%s wrapping %s to %s", account1_, amount_, account2_); @@ -645,7 +729,7 @@ contract ProtocolIntegrationTests is TestBase { if (amount_ >= account1Balance_) { // console2.log("%s unwrapping all their wM to %s", account1_, account2_); - _unwrap(account1_, account2_); + _unwrap(account1_, account2_, account1Balance_ - 1); // -1 to account for rounding up for earners. } else { // console2.log("%s unwrapping %s to %s", account1_, amount_, account2_); @@ -658,13 +742,13 @@ contract ProtocolIntegrationTests is TestBase { // 10% chance to start/stop earning if ((seed_ % 100) >= 10) { if (_wrappedMToken.isEarning(account1_)) { - _removeFomList(_EARNERS_LIST, account1_); + _removeFromList(_EARNERS_LIST_NAME, account1_); // console2.log("%s stopping earning", account1_); _wrappedMToken.stopEarningFor(account1_); } else { - _addToList(_EARNERS_LIST, account1_); + _addToList(_EARNERS_LIST_NAME, account1_); // console2.log("%s starting earning", account1_); diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 8936b69..733f558 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -1,29 +1,63 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.23; +pragma solidity 0.8.26; -import { Test } from "../../lib/forge-std/src/Test.sol"; +import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; +import { IERC20Extended } from "../../lib/common/src/interfaces/IERC20Extended.sol"; +import { IERC712 } from "../../lib/common/src/interfaces/IERC712.sol"; -import { IMTokenLike, IRegistrarLike } from "./vendor/protocol/Interfaces.sol"; +import { Test } from "../../lib/forge-std/src/Test.sol"; +import { Proxy } from "../../lib/common/src/Proxy.sol"; -import { Proxy } from "../../src/Proxy.sol"; +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; -contract TestBase is Test { +import { DeployBase } from "../../script/DeployBase.sol"; + +import { IMTokenLike, IRegistrarLike, ISwapFacilityLike } from "./vendor/protocol/Interfaces.sol"; + +import { MockSwapFacility } from "../utils/Mocks.sol"; + +contract TestBase is Test, DeployBase { + uint256 public mainnetFork; + IMTokenLike internal constant _mToken = IMTokenLike(0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b); address internal constant _minterGateway = 0xf7f9638cb444D65e5A40bF5ff98ebE4ff319F04E; address internal constant _registrar = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; - address internal constant _vault = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; + address internal constant _excessDestination = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // vault address internal constant _standardGovernor = 0xB024aC5a7c6bC92fbACc8C3387E628a07e1Da016; - address internal constant _mSource = 0x20b3a4119eAB75ffA534aC8fC5e9160BdcaF442b; + address internal constant _mSource = 0x3f0376da3Ae4313E7a5F1dA184BAFC716252d759; + address internal constant _wmSource = 0xfF95c5f35F4ffB9d5f596F898ac1ae38D62749c2; + + IWrappedMToken internal constant _wrappedMToken = IWrappedMToken(0x437cc33344a0B27A429f795ff6B469C72698B291); + + bytes32 internal constant _EARNERS_LIST_NAME = "earners"; + bytes32 internal constant _MIGRATOR_V1_PREFIX = "wm_migrator_v1"; + bytes32 internal constant _CLAIM_OVERRIDE_RECIPIENT_PREFIX = "wm_claim_override_recipient"; + bytes32 internal constant _EARNER_STATUS_ADMIN_LIST = "wm_earner_status_admins"; + bytes32 internal constant _M_SWAPPER_ROLE = keccak256("M_SWAPPER_ROLE"); + + // USDC on Ethereum Mainnet + address internal constant _USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + // Large USDC holder on Ethereum Mainnet + address internal constant _USDC_SOURCE = 0x01b8697695EAb322A339c4bf75740Db75dc9375E; - bytes32 internal constant _EARNERS_LIST = "earners"; + // DAI on Ethereum Mainnet + address internal constant _DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; - address internal _wrappedMTokenImplementation; + // Large DAI holder on Ethereum Mainnet + address internal constant _DAI_SOURCE = 0xD1668fB5F690C59Ab4B0CAbAd0f8C1617895052B; - WrappedMToken internal _wrappedMToken; + address internal _migrationAdmin = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + address internal _m0Deployer = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _admin = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _freezeManager = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _pauser = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _forcedTransferManager = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _excessManager = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; address internal _alice = makeAddr("alice"); address internal _bob = makeAddr("bob"); @@ -36,15 +70,68 @@ contract TestBase is Test { address internal _ivan = makeAddr("ivan"); address internal _judy = makeAddr("judy"); + uint256 internal _aliceKey = _makeKey("alice"); + address[] internal _accounts = [_alice, _bob, _carol, _dave, _eric, _frank, _grace, _henry, _ivan, _judy]; - address internal _migrationAdmin = makeAddr("migrationAdmin"); + address internal _wrappedMTokenImplementationV2; + address internal _wrappedMTokenMigratorV1; - bytes32 internal constant _CLAIM_OVERRIDE_RECIPIENT_PREFIX = "wm_claim_override_recipient"; + address internal _swapFacility = 0xB6807116b3B1B321a390594e31ECD6e0076f6278; + + address[] internal _earners = [ + 0x437cc33344a0B27A429f795ff6B469C72698B291, + 0x0502d65f26f45d17503E4d34441F5e73Ea143033, + 0x061110360ba50E19139a1Bf2EaF4004FB0dD31e8, + 0x9106CBf2C882340b23cC40985c05648173E359e7, + 0x846E7F810E08F1E2AF2c5AfD06847cc95F5CaE1B, + 0x967B10c27454CC5b1b1Eeb163034ACdE13Fe55e2, + 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, + 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90, + 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, + 0x184d597Be309e11650ca6c935B483DcC05551578, + 0xA259E266a43F3070CecD80F05C8947aB93c074Ba, + 0x0f71a8e95A918A4A984Ad3841414cD00D9C13e7d, + 0xf3CfA6e51b2B580AE6Ad71e2D719Ab09e4A0D7aa, + 0x56721131d21a170fBb084734DcC399A278234298, + 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, + 0xDeD796De6a14E255487191963dEe436c45995813, + 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, + 0x8F9139Fe15E561De5fCe39DB30856924Dd67Af0e, + 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, + 0xB65a66621D7dE34afec9b9AC0755133051550dD7, + 0xcAD001c30E96765aC90307669d578219D4fb1DCe, + 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70, + 0x9c6e67fA86138Ab49359F595BfE4Fb163D0f16cc, + 0xABFD9948933b975Ee9a668a57C776eCf73F6D840, + 0x7FDA203f6F77545548E984133be62693bCD61497, + 0xa8687A15D4BE32CC8F0a8a7B9704a4C3993D9613, + 0x3f0376da3Ae4313E7a5F1dA184BAFC716252d759, + 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec, + 0xcEa14C3e9Afc5822d44ADe8d006fCFBAb60f7a21, + 0x81ad394C0Fa87e99Ca46E1aca093BEe020f203f4, + 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, + 0x13Ccb6E28F22E2f6783BaDedCe32cc74583A3647, + 0x985DE23260743c2c2f09BFdeC50b048C7a18c461, + 0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd, + 0x20b3a4119eAB75ffA534aC8fC5e9160BdcaF442b + ]; function setUp() public virtual { - _wrappedMTokenImplementation = address(new WrappedMToken(address(_mToken), _migrationAdmin)); - _wrappedMToken = WrappedMToken(address(new Proxy(_wrappedMTokenImplementation))); + mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL"), 23_170_985); + } + + function _getSource(address token_) internal pure returns (address source_) { + if (token_ == _USDC) return _USDC_SOURCE; + + if (token_ == _DAI) return _DAI_SOURCE; + + revert(); + } + + function _give(address token_, address account_, uint256 amount_) internal { + vm.prank(_getSource(token_)); + IERC20(token_).transfer(account_, amount_); } function _addToList(bytes32 list_, address account_) internal { @@ -52,7 +139,7 @@ contract TestBase is Test { IRegistrarLike(_registrar).addToList(list_, account_); } - function _removeFomList(bytes32 list_, address account_) internal { + function _removeFromList(bytes32 list_, address account_) internal { vm.prank(_standardGovernor); IRegistrarLike(_registrar).removeFromList(list_, account_); } @@ -62,34 +149,29 @@ contract TestBase is Test { _mToken.transfer(account_, amount_); } + function _giveWM(address account_, uint256 amount_) internal { + vm.prank(_wmSource); + _wrappedMToken.transfer(account_, amount_); + } + function _giveEth(address account_, uint256 amount_) internal { vm.deal(account_, amount_); } function _wrap(address account_, address recipient_, uint256 amount_) internal { vm.prank(account_); - _mToken.approve(address(_wrappedMToken), amount_); - - vm.prank(account_); - _wrappedMToken.wrap(recipient_, amount_); - } - - function _wrap(address account_, address recipient_) internal { - vm.prank(account_); - _mToken.approve(address(_wrappedMToken), type(uint256).max); + _mToken.approve(_swapFacility, amount_); vm.prank(account_); - _wrappedMToken.wrap(recipient_); + ISwapFacilityLike(_swapFacility).swapInM(address(_wrappedMToken), amount_, recipient_); } function _unwrap(address account_, address recipient_, uint256 amount_) internal { vm.prank(account_); - _wrappedMToken.unwrap(recipient_, amount_); - } + _wrappedMToken.approve(_swapFacility, amount_); - function _unwrap(address account_, address recipient_) internal { vm.prank(account_); - _wrappedMToken.unwrap(recipient_); + ISwapFacilityLike(_swapFacility).swapOutM(address(_wrappedMToken), amount_, recipient_); } function _transferWM(address sender_, address recipient_, uint256 amount_) internal { @@ -110,4 +192,72 @@ contract TestBase is Test { function _setClaimOverrideRecipient(address account_, address recipient_) internal { _set(keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_PREFIX, account_)), bytes32(uint256(uint160(recipient_)))); } + + function _deployV2Components() internal { + _wrappedMTokenImplementationV2 = address( + new WrappedMToken(address(_mToken), _registrar, _swapFacility, _migrationAdmin) + ); + + _wrappedMTokenMigratorV1 = _deployMigrator( + _wrappedMTokenImplementationV2, + _earners, + _excessDestination, + UpgradeRoles({ + admin: _admin, + freezeManager: _freezeManager, + pauser: _pauser, + forcedTransferManager: _forcedTransferManager, + excessManager: _excessManager + }) + ); + } + + function _migrate() internal { + _set( + keccak256(abi.encode(_MIGRATOR_V1_PREFIX, address(_wrappedMToken))), + bytes32(uint256(uint160(_wrappedMTokenMigratorV1))) + ); + + _wrappedMToken.migrate(); + } + + function _migrateFromAdmin() internal { + vm.prank(_migrationAdmin); + _wrappedMToken.migrate(_wrappedMTokenMigratorV1); + } + + /* ============ utils ============ */ + + function _makeKey(string memory name_) internal returns (uint256 key_) { + (, key_) = makeAddrAndKey(name_); + } + + function _getPermit( + address account_, + uint256 signerPrivateKey_, + uint256 amount_, + uint256 nonce_, + uint256 deadline_ + ) internal view returns (uint8 v_, bytes32 r_, bytes32 s_) { + return + vm.sign( + signerPrivateKey_, + keccak256( + abi.encodePacked( + "\x19\x01", + IERC712(address(_mToken)).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + IERC20Extended(address(_mToken)).PERMIT_TYPEHASH(), + account_, + address(_wrappedMToken), + amount_, + nonce_, + deadline_ + ) + ) + ) + ) + ); + } } diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index 0c3a05d..25947ac 100644 --- a/test/integration/UniswapV3.t.sol +++ b/test/integration/UniswapV3.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; - -import { TestBase } from "./TestBase.sol"; +pragma solidity 0.8.26; import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; @@ -13,7 +11,9 @@ import { ISwapRouter } from "./vendor/uniswap-v3/Interfaces.sol"; -import { Utils } from "./vendor/uniswap-v3/Utils.sol"; +import { Utils as UniswapUtils } from "./vendor/uniswap-v3/Utils.sol"; + +import { TestBase } from "./TestBase.sol"; contract UniswapV3IntegrationTests is TestBase { // Uniswap V3 Position Manager on Ethereum Mainnet @@ -26,13 +26,11 @@ contract UniswapV3IntegrationTests is TestBase { // Uniswap V3 Router on Ethereum Mainnet ISwapRouter internal constant _router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - // USDC on Ethereum Mainnet - address internal constant _USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - // Uniswap V3 stable pair fee uint24 internal constant _POOL_FEE = 100; // 0.01% in bps - address internal _pool; + address internal _pool = 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288; + address internal _poolClaimRecipient; uint256 internal _wrapperBalanceOfM; @@ -43,6 +41,7 @@ contract UniswapV3IntegrationTests is TestBase { uint256 internal _daveBalanceOfUSDC; uint256 internal _poolBalanceOfWM; + uint256 internal _poolClaimRecipientBalanceOfWM; uint256 internal _aliceBalanceOfWM; uint256 internal _bobBalanceOfWM; uint256 internal _carolBalanceOfWM; @@ -51,219 +50,142 @@ contract UniswapV3IntegrationTests is TestBase { uint256 internal _poolAccruedYield; uint256 internal _bobAccruedYield; + int256 internal _excess; + function setUp() public override { super.setUp(); - _addToList(_EARNERS_LIST, address(_wrappedMToken)); + vm.selectFork(mainnetFork); - _wrappedMToken.enableEarning(); + _deployV2Components(); + _migrate(); - _pool = _createPool(); - } + _poolClaimRecipient = _wrappedMToken.claimRecipientFor(_pool); - function test_initialState() external view { - assertTrue(_mToken.isEarning(address(_wrappedMToken))); - assertEq(_wrappedMToken.isEarningEnabled(), true); - assertFalse(_wrappedMToken.isEarning(_pool)); + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + _poolBalanceOfUSDC = IERC20(_USDC).balanceOf(_pool); + _poolBalanceOfWM = _wrappedMToken.balanceOf(_pool); + _poolClaimRecipientBalanceOfWM = _wrappedMToken.balanceOf(_poolClaimRecipient); + _poolAccruedYield = _wrappedMToken.accruedYieldOf(_pool); + _excess = _wrappedMToken.excess(); } - function test_uniswapV3_nonEarning() external { - /* ============ Alice Mints New LP Position ============ */ - - _giveM(_alice, 1_000_100e6); - _wrap(_alice, _alice, 1_000_100e6); - - assertEq(_mToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_000_099_999999); - - deal(_USDC, _alice, 1_000_100e6); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_100e6); - - _mintNewPosition(_alice, _alice, 1_000_000e6); - - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000_000e6); - - /* ============ First 1-Year Time Warp ============ */ - - // Move 1 year forward and check that no yield has accrued. - vm.warp(vm.getBlockTimestamp() + 365 days); - - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 51_276_223485); - - // `startEarningFor` hasn't been called so no wM yield has accrued in the pool. - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); - - // But excess yield has accrued in the wrapped M contract. - assertEq(_wrappedMToken.excess(), 51_276_223483); - - // USDC balance is unchanged. - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); - - // TODO: Bob Swaps USDC for wM - // TODO: Second 1-Year Time Warp - // TODO: Dave Swaps wM for USDC + function test_state() external view { + assertTrue(_mToken.isEarning(address(_wrappedMToken))); + assertTrue(_wrappedMToken.isEarningEnabled()); + assertTrue(_wrappedMToken.isEarning(_pool)); } function test_uniswapV3_earning() external { /* ============ Alice Mints New LP Position ============ */ - _giveM(_alice, 1_000_100e6); - _wrap(_alice, _alice, 1_000_100e6); - - assertEq(_mToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 1_000_099_999999); - - deal(_USDC, _alice, 1_000_100e6); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_100e6); - - _mintNewPosition(_alice, _alice, 1_000_000e6); - - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); + _giveWM(_alice, 1_001e6); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC = 1_000_000e6); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_001e6); - /* ============ Pool Becomes An Earner ============ */ + _give(_USDC, _alice, 1_001e6); - _setClaimOverrideRecipient(_pool, _carol); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_001e6); - _addToList(_EARNERS_LIST, _pool); - _wrappedMToken.startEarningFor(_pool); + _mintNewPosition(_alice, _alice, 1_000e6); - assertTrue(_wrappedMToken.isEarning(_pool)); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 998_553467); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 998_553467); - assertEq(_wrappedMToken.claimOverrideRecipientFor(_pool), _carol); + // The mint has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); + /* ============ First 1-Year Time Warp ============ */ // Move 1 year forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 365 days); - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 51_276_223485); - // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 51_271_096375); - - // No excess yield has accrued in the wrapped M contract since the pool is the only earner. - assertEq(_wrappedMToken.excess(), 5_127108); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 102_522_707768); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); /* ============ Bob Swaps Exact USDC for wM ============ */ - deal(_USDC, _bob, 100_000e6); + _give(_USDC, _bob, 1_000e6); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 100_000e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 1_000e6); - uint256 swapAmountOut_ = _swapExactInput(_bob, _bob, _USDC, address(_wrappedMToken), 100_000e6); + uint256 swapAmountOut_ = _swapExactInput(_bob, _bob, _USDC, address(_wrappedMToken), 1_000e6); // Check pool liquidity after the swap - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 100_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 100_000e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); assertEq(_wrappedMToken.balanceOf(_bob), swapAmountOut_); - // The swap has triggered a wM transfer and the yield has been claimed to carol for the pool. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM -= swapAmountOut_); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); /* ============ Second 1-Year Time Warp ============ */ // Move 1 year forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 365 days); - // Wrapped M is earning M and has accrued yield. - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 53_905_211681); - // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 46_610_511346); - - // No excess yield has accrued in the wrapped M contract since the pool is the only earner. - assertEq(_wrappedMToken.excess(), 7299_827441); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 106_824_553128); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); /* ============ Dave (Earner) Swaps Exact wM for USDC ============ */ - _addToList(_EARNERS_LIST, _dave); + _addToList(_EARNERS_LIST_NAME, _dave); _wrappedMToken.startEarningFor(_dave); - _giveM(_dave, 100_100e6); - _wrap(_dave, _dave, 100_100e6); + _giveWM(_dave, 1_001e6); - assertEq(_mToken.balanceOf(_dave), 0); - assertEq(_wrappedMToken.balanceOf(_dave), _daveBalanceOfWM += 100_099_999999); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 100_099_999999); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalanceOfWM += 1_001e6); - swapAmountOut_ = _swapExactInput(_dave, _dave, address(_wrappedMToken), _USDC, 100_000e6); + swapAmountOut_ = _swapExactInput(_dave, _dave, address(_wrappedMToken), _USDC, 1_000e6); // Check pool liquidity after the swap. assertEq(IERC20(_USDC).balanceOf(_dave), _daveBalanceOfUSDC += swapAmountOut_); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= swapAmountOut_); - // The swap has triggered a wM transfer and the yield has been claimed to carol for the pool. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 100_000e6); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); } function testFuzz_uniswapV3_earning(uint256 aliceAmount_, uint256 bobUsdc_, uint256 daveWrappedM_) public { - aliceAmount_ = bound(aliceAmount_, 1e6, _mToken.balanceOf(_mSource) / 10); - bobUsdc_ = bound(bobUsdc_, 1e6, 1e12); - daveWrappedM_ = bound(daveWrappedM_, 1e6, _mToken.balanceOf(_mSource) / 10); + aliceAmount_ = bound(aliceAmount_, 10e6, _wrappedMToken.balanceOf(_wmSource) / 10); + bobUsdc_ = bound(bobUsdc_, 1e6, aliceAmount_ / 3); + daveWrappedM_ = bound(daveWrappedM_, 1e6, aliceAmount_ / 3); /* ============ Alice Mints New LP Position ============ */ - _giveM(_alice, aliceAmount_ + 2); - _wrap(_alice, _alice, aliceAmount_ + 2); - - assertApproxEqAbs(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += aliceAmount_, 3); - - deal(_USDC, _alice, aliceAmount_); - - _mintNewPosition(_alice, _alice, aliceAmount_); + _giveWM(_alice, _aliceBalanceOfWM += aliceAmount_); - assertApproxEqAbs(_wrappedMToken.balanceOf(_alice), 0, 10); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += aliceAmount_); + _give(_USDC, _alice, _aliceBalanceOfUSDC += aliceAmount_); - assertEq(IERC20(_USDC).balanceOf(_alice), 0); - assertEq(IERC20(_USDC).balanceOf(_pool), aliceAmount_); + (, , uint256 amount0_, uint256 amount1_) = _mintNewPosition(_alice, _alice, aliceAmount_); - /* ============ Pool Becomes An Earner ============ */ + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= amount0_); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += amount0_); - _setClaimOverrideRecipient(_pool, _carol); + // The mint has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); - _addToList(_EARNERS_LIST, _pool); - _wrappedMToken.startEarningFor(_pool); - - // Check that the pool is earning WM - assertTrue(_wrappedMToken.isEarning(_pool)); - - assertEq(_wrappedMToken.claimOverrideRecipientFor(_pool), _carol); - - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= amount1_); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += amount1_); /* ============ First 1-Year Time Warp ============ */ @@ -274,44 +196,35 @@ contract UniswapV3IntegrationTests is TestBase { uint128 newIndex_ = _mToken.currentIndex(); - // Wrapped M is earning M and has accrued yield. - assertApproxEqAbs( - _mToken.balanceOf(address(_wrappedMToken)), - _wrapperBalanceOfM = (_wrapperBalanceOfM * newIndex_) / index_, - 10 - ); - // `startEarningFor` has been called so WM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); assertApproxEqAbs( - _poolAccruedYield += _wrappedMToken.accruedYieldOf(_pool), - (_poolBalanceOfWM * newIndex_) / index_ - _poolBalanceOfWM, + _wrappedMToken.accruedYieldOf(_pool), + ((_poolBalanceOfWM + _poolAccruedYield) * newIndex_) / index_ - _poolBalanceOfWM, 10 ); - // No excess yield has accrued in the wrapped M contract since the pool is the only earner. - assertApproxEqAbs(_wrappedMToken.excess(), 0, 10); + _poolAccruedYield = _wrappedMToken.accruedYieldOf(_pool); // _USDC balance is unchanged since no swap has been performed. - assertEq(IERC20(_USDC).balanceOf(_pool), aliceAmount_); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); /* ============ Bob Swaps Exact USDC for wM ============ */ - deal(_USDC, _bob, bobUsdc_); + _give(_USDC, _bob, _bobBalanceOfUSDC += bobUsdc_); uint256 swapOutWM_ = _swapExactInput(_bob, _bob, _USDC, address(_wrappedMToken), bobUsdc_); // Check pool liquidity after the swap - assertEq(IERC20(_USDC).balanceOf(_bob), 0); - assertEq(IERC20(_USDC).balanceOf(_pool), aliceAmount_ + bobUsdc_); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= bobUsdc_); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += bobUsdc_); assertEq(_wrappedMToken.balanceOf(_bob), swapOutWM_); - // The swap has triggered a wM transfer and the yield has been claimed to carol for the pool. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM -= swapOutWM_); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); /* ============ Second 1-Year Time Warp ============ */ @@ -322,139 +235,106 @@ contract UniswapV3IntegrationTests is TestBase { newIndex_ = _mToken.currentIndex(); - // Wrapped M is earning M and has accrued yield. - assertApproxEqAbs( - _mToken.balanceOf(address(_wrappedMToken)), - _wrapperBalanceOfM = (_wrapperBalanceOfM * newIndex_) / index_, - 10 - ); - // `startEarningFor` has been called so WM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); assertApproxEqAbs( - _poolAccruedYield += _wrappedMToken.accruedYieldOf(_pool), - (_poolBalanceOfWM * newIndex_) / index_ - _poolBalanceOfWM, + _wrappedMToken.accruedYieldOf(_pool), + ((_poolBalanceOfWM + _poolAccruedYield) * newIndex_) / index_ - _poolBalanceOfWM, 10 ); + _poolAccruedYield = _wrappedMToken.accruedYieldOf(_pool); + // USDC balance is unchanged since no swap has been performed. - assertEq(IERC20(_USDC).balanceOf(_pool), aliceAmount_ + bobUsdc_); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); /* ============ Dave (Earner) Swaps Exact wM for USDC ============ */ - _addToList(_EARNERS_LIST, _dave); + _addToList(_EARNERS_LIST_NAME, _dave); _wrappedMToken.startEarningFor(_dave); - // NOTE: Give 2 more so that rounding errors do not prevent _swap. - _giveM(_dave, daveWrappedM_ + 2); - _wrap(_dave, _dave, daveWrappedM_ + 2); - - assertApproxEqAbs(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += daveWrappedM_, 6); + _giveWM(_dave, daveWrappedM_); uint256 swapOutUSDC_ = _swapExactInput(_dave, _dave, address(_wrappedMToken), _USDC, daveWrappedM_); // Check pool liquidity after the swap. assertEq(IERC20(_USDC).balanceOf(_dave), swapOutUSDC_); - assertEq(IERC20(_USDC).balanceOf(_pool), aliceAmount_ + bobUsdc_ - swapOutUSDC_); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= swapOutUSDC_); - // The swap has triggered a wM transfer and the yield has been claimed to carol for the pool. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += daveWrappedM_); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); } function test_uniswapV3_exactInputOrOutputForEarnersAndNonEarners() public { - /* ============ Pool Becomes An Earner ============ */ - - _setClaimOverrideRecipient(_pool, _carol); - - _addToList(_EARNERS_LIST, _pool); - _wrappedMToken.startEarningFor(_pool); - - assertTrue(_wrappedMToken.isEarning(_pool)); + /* ============ Alice Mints New LP Position ============ */ - assertEq(_wrappedMToken.claimOverrideRecipientFor(_pool), _carol); + _giveWM(_alice, 1_001e6); - /* ============ Alice Mints New LP Position ============ */ + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_001e6); - _giveM(_alice, 1_000_100e6); - _wrap(_alice, _alice, _mToken.balanceOf(_alice)); + _give(_USDC, _alice, 1_001e6); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_099_999999); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_001e6); - deal(_USDC, _alice, 1_000_100e6); + _mintNewPosition(_alice, _alice, 1_000e6); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_100e6); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 998_553467); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 998_553467); - _mintNewPosition(_alice, _alice, 1_000_000e6); + // The mint has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC = 1_000_000e6); - - // Totals checks: pool is the only earner. - assertEq(_wrappedMToken.totalEarningSupply(), _poolBalanceOfWM); - assertEq(_wrappedMToken.totalNonEarningSupply(), _aliceBalanceOfWM); - assertEq(_wrappedMToken.totalAccruedYield(), _poolAccruedYield); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); /* ============ 10-Day Time Warp ============ */ // Move 10 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 10 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1_370_801702); - - // Totals checks. - assertEq(_wrappedMToken.totalEarningSupply(), _poolBalanceOfWM); - assertEq(_wrappedMToken.totalNonEarningSupply(), _aliceBalanceOfWM); - assertEq(_wrappedMToken.totalAccruedYield(), _poolAccruedYield + 1); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 2_752_525465); /* ============ 2 Non-Earners and 2 Earners are Initialized ============ */ - _giveM(_bob, 100_100e6); - _wrap(_bob, _bob, 100_100e6); - - _giveM(_dave, 100_100e6); - _wrap(_dave, _dave, 100_100e6); + _giveWM(_bob, 1_001e6); + _giveWM(_dave, 1_001e6); + _giveWM(_eric, 1_001e6); + _giveWM(_frank, 1_001e6); - _addToList(_EARNERS_LIST, _eric); + _addToList(_EARNERS_LIST_NAME, _eric); _wrappedMToken.startEarningFor(_eric); - _giveM(_eric, 100_100e6); - _wrap(_eric, _eric, 100_100e6); - - _addToList(_EARNERS_LIST, _frank); + _addToList(_EARNERS_LIST_NAME, _frank); _wrappedMToken.startEarningFor(_frank); - _giveM(_frank, 100_100e6); - _wrap(_frank, _frank, 100_100e6); - /* ============ Bob (Non-Earner) Swaps Exact wM for USDC ============ */ - _swapExactInput(_bob, _bob, address(_wrappedMToken), _USDC, 100_000e6); + _swapExactInput(_bob, _bob, address(_wrappedMToken), _USDC, 1_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 100_000e6); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); - // Check that carol received yield. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); + + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); /* ============ 1-Day Time Warp ============ */ // Move 1 day forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 1 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 150_695251); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 275_538415); // Claim yield for the pool and check that carol received yield. _wrappedMToken.claimFor(_pool); - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); /* ============ 5-Day Time Warp ============ */ @@ -462,174 +342,133 @@ contract UniswapV3IntegrationTests is TestBase { // Move 5 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 5 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 753_682738); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1_376_440059); /* ============ Eric (Earner) Swaps Exact wM for USDC ============ */ - _swapExactInput(_eric, _eric, address(_wrappedMToken), _USDC, 100_000e6); + _swapExactInput(_eric, _eric, address(_wrappedMToken), _USDC, 1_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 100_000e6); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); - // Check that carol received yield. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); + + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); /* ============ 3-Day Time Warp ============ */ // Move 3 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 3 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 493_252029); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 826_580864); /* ============ Dave (Non-Earner) Swaps wM for Exact USDC ============ */ // Option 3: Exact output parameter swap from non-earner - uint256 daveOutput_ = _swapExactOutput(_dave, _dave, address(_wrappedMToken), _USDC, 10_000e6); + uint256 daveOutput_ = _swapExactOutput(_dave, _dave, address(_wrappedMToken), _USDC, 1_000e6); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += daveOutput_); - // Check that carol received yield. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); + + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= 1); // Rounding error /* ============ 7-Day Time Warp ============ */ // Move 7 day forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 7 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1_165_220368); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1_930_581718); /* ============ Frank (Earner) Swaps wM for Exact USDC ============ */ - uint256 frankOutput_ = _swapExactOutput(_frank, _frank, address(_wrappedMToken), _USDC, 10_000e6); + uint256 frankOutput_ = _swapExactOutput(_frank, _frank, address(_wrappedMToken), _USDC, 1_000e6); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += frankOutput_); - // Check that carol received yield. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); - } - - function test_uniswapV3_increaseDecreaseLiquidityAndFees() public { - /* ============ Pool Becomes An Earner ============ */ + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); - _setClaimOverrideRecipient(_pool, _carol); - - _addToList(_EARNERS_LIST, _pool); - _wrappedMToken.startEarningFor(_pool); - - assertTrue(_wrappedMToken.isEarning(_pool)); - - assertEq(_wrappedMToken.claimOverrideRecipientFor(_pool), _carol); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); + } + function test_uniswapV3_increaseDecreaseLiquidity() public { /* ============ Fund Alice (Non-Earner) and Bob (Earner) ============ */ - _giveM(_alice, 2_000_100e6); - _wrap(_alice, _alice, _mToken.balanceOf(_alice)); + _giveWM(_alice, 2_001e6); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 2_000_099_999999); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 2_001e6); - deal(_USDC, _alice, 2_000_100e6); + _give(_USDC, _alice, 2_001e6); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 2_000_100e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 2_001e6); - _addToList(_EARNERS_LIST, _bob); + _addToList(_EARNERS_LIST_NAME, _bob); _wrappedMToken.startEarningFor(_bob); - _giveM(_bob, 2_000_100e6); - _wrap(_bob, _bob, _mToken.balanceOf(_bob)); + _giveWM(_bob, 2_001e6); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 2_000_100_000000); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 2_001e6); - deal(_USDC, _bob, 2_000_100e6); + _give(_USDC, _bob, 2_001e6); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 2_000_100e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += 2_001e6); /* ============ Alice (Non-Earner) and Bob (Earner) Mint New LP Positions ============ */ - (uint256 aliceTokenId_, , , ) = _mintNewPosition(_alice, _alice, 1_000_000e6); + (uint256 aliceTokenId_, , , ) = _mintNewPosition(_alice, _alice, 1_000e6); + + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 998_553467); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 998_553467); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000_000e6); + (uint256 bobTokenId_, , , ) = _mintNewPosition(_bob, _bob, 1_000e6); - (uint256 bobTokenId_, , , ) = _mintNewPosition(_bob, _bob, 1_000_000e6); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 998_553467); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 998_553467); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000_000e6); + _poolClaimRecipientBalanceOfWM = _wrappedMToken.balanceOf(_poolClaimRecipient); /* ============ 10-Day Time Warp ============ */ // Move 10 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 10 days); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 550_891667); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1_101_673168); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_140416); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 2_753_661453); /* ============ Dave (Non-Earner) Swaps Exact wM for USDC ============ */ - _giveM(_dave, 100_100e6); - _wrap(_dave, _dave, 100_100e6); + _giveWM(_dave, 1_001e6); - assertEq(_wrappedMToken.balanceOf(_dave), _daveBalanceOfWM += 100_099_999999); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalanceOfWM += 1_001e6); - _swapExactInput(_dave, _dave, address(_wrappedMToken), _USDC, 100_000e6); + _swapExactInput(_dave, _dave, address(_wrappedMToken), _USDC, 1_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= 95_229_024894); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= 999_970585); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 100_000e6); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); - // Check that carol received yield. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); - - /* ============ Alice (Non-Earner) And Bob (Earner) Collect Fees ============ */ - - (uint256 aliceAmountWM_, uint256 aliceAmountUSDC_) = _collect(_alice, aliceTokenId_); - - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += aliceAmountWM_); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM -= aliceAmountWM_); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += aliceAmountUSDC_); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= aliceAmountUSDC_); - - (uint256 bobAmountWM_, uint256 bobAmountUSDC_) = _collect(_bob, bobTokenId_); - - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += bobAmountWM_ + _bobAccruedYield); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM -= bobAmountWM_); + // The swap has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= _bobAccruedYield); - - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += bobAmountUSDC_); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= bobAmountUSDC_); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); /* ============ Alice (Non-Earner) Decreases Liquidity And Bob (Earner) Increases Liquidity ============ */ - (aliceAmountWM_, aliceAmountUSDC_) = _decreaseLiquidityCurrentRange(_alice, aliceTokenId_, 500_000e6); - - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += aliceAmountWM_); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM -= aliceAmountWM_); - - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += aliceAmountUSDC_); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= aliceAmountUSDC_); - - (, bobAmountWM_, bobAmountUSDC_) = _increaseLiquidityCurrentRange(_bob, bobTokenId_, 100_000e6); - - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= bobAmountWM_); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += bobAmountWM_); - - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= bobAmountUSDC_); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += bobAmountUSDC_); + _decreaseLiquidityCurrentRange(_alice, aliceTokenId_, 500e6); + _increaseLiquidityCurrentRange(_bob, bobTokenId_, 1_000e6); } function _createPool() internal returns (address pool_) { pool_ = _factory.createPool(address(_wrappedMToken), _USDC, _POOL_FEE); - IUniswapV3Pool(pool_).initialize(Utils.encodePriceSqrt(1, 1)); + IUniswapV3Pool(pool_).initialize(UniswapUtils.encodePriceSqrt(1, 1)); } function _approve(address token_, address account_, address spender_, uint256 amount_) internal { @@ -655,8 +494,8 @@ contract UniswapV3IntegrationTests is TestBase { token0: address(_wrappedMToken), token1: _USDC, fee: _POOL_FEE, - tickLower: Utils.MIN_TICK, - tickUpper: Utils.MAX_TICK, + tickLower: -1000, + tickUpper: 1000, amount0Desired: amount_, amount1Desired: amount_, amount0Min: 0, diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol new file mode 100644 index 0000000..b8eadf0 --- /dev/null +++ b/test/integration/Upgrade.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { Test } from "../../lib/forge-std/src/Test.sol"; + +import { + IAccessControl +} from "../../lib/common/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/access/IAccessControl.sol"; + +import { + Initializable +} from "../../lib/common/lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; + +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; + +import { DeployBase } from "../../script/DeployBase.sol"; + +import { WrappedMToken } from "../../src/WrappedMToken.sol"; + +contract UpgradeTests is Test, DeployBase { + WrappedMToken internal constant _WRAPPED_M_TOKEN = WrappedMToken(0x437cc33344a0B27A429f795ff6B469C72698B291); + address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; + address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; + address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault + address internal constant _SWAP_FACILITY = 0xB6807116b3B1B321a390594e31ECD6e0076f6278; + + address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + uint64 internal constant _DEPLOYER_NONCE = 195; + + address internal constant _ADMIN = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal constant _FREEZE_MANAGER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal constant _PAUSER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal constant _FORCED_TRANSFER_MANAGER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal constant _EXCESS_MANAGER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + address[] internal _earners = [ + 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, + 0xfF95c5f35F4ffB9d5f596F898ac1ae38D62749c2, + 0x0502d65f26f45d17503E4d34441F5e73Ea143033, + 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, + 0x81ad394C0Fa87e99Ca46E1aca093BEe020f203f4, + 0xfE940BFE535013a52e8e2DF9644f95E3C94fa14B, + 0xE0663f2372cAa1459b7ade90812Dc737CE587FA6, + 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, + 0x9c6e67fA86138Ab49359F595BfE4Fb163D0f16cc, + 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, + 0xB65a66621D7dE34afec9b9AC0755133051550dD7, + 0xcAD001c30E96765aC90307669d578219D4fb1DCe, + 0xDeD796De6a14E255487191963dEe436c45995813, + 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90, + 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, + 0x13Ccb6E28F22E2f6783BaDedCe32cc74583A3647, + 0x985DE23260743c2c2f09BFdeC50b048C7a18c461, + 0x7db685961F97c847A4C815D43E9cc0E5647328b9, + 0x48Afe17cB6363fD1aaeA50a8CB652C5978972c96, + 0xE72Fe64840F4EF80E3Ec73a1c749491b5c938CB9, + 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, + 0xB50A1f651A5ACb2679c8f679D782c728f3702E53, + 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70, + 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec + ]; + + function setUp() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 23_170_985); + } + + function test_upgrade() external { + vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); + + (address expectedWrappedMTokenImplementation_, address expectedWrappedMTokenMigrator_) = mockDeployUpgrade( + _DEPLOYER, + _DEPLOYER_NONCE + ); + + vm.startPrank(_DEPLOYER); + (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) = deployUpgrade( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _SWAP_FACILITY, + _WRAPPED_M_MIGRATION_ADMIN, + _earners, + UpgradeRoles({ + admin: _ADMIN, + freezeManager: _FREEZE_MANAGER, + pauser: _PAUSER, + forcedTransferManager: _FORCED_TRANSFER_MANAGER, + excessManager: _EXCESS_MANAGER + }) + ); + vm.stopPrank(); + + // Wrapped M Token Implementation assertions + assertEq(wrappedMTokenImplementation_, expectedWrappedMTokenImplementation_); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).swapFacility(), _SWAP_FACILITY); + + // Migrator assertions + assertEq(wrappedMTokenMigrator_, expectedWrappedMTokenMigrator_); + + uint240 totalEarningSupply_ = IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(); + uint256[] memory balancesWithYield_ = new uint256[](_earners.length); + + for (uint256 index_; index_ < _earners.length; ++index_) { + balancesWithYield_[index_] = IWrappedMToken(_WRAPPED_M_TOKEN).balanceWithYieldOf(_earners[index_]); + } + + vm.prank(_WRAPPED_M_TOKEN.migrationAdmin()); + _WRAPPED_M_TOKEN.migrate(wrappedMTokenMigrator_); + + // Wrapped M Token Proxy assertions + assertEq(_WRAPPED_M_TOKEN.migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); + assertEq(_WRAPPED_M_TOKEN.mToken(), _M_TOKEN); + assertEq(_WRAPPED_M_TOKEN.registrar(), _REGISTRAR); + assertEq(_WRAPPED_M_TOKEN.excessDestination(), _EXCESS_DESTINATION); + assertEq(_WRAPPED_M_TOKEN.swapFacility(), _SWAP_FACILITY); + assertEq(_WRAPPED_M_TOKEN.implementation(), wrappedMTokenImplementation_); + + assertTrue(IAccessControl(address(_WRAPPED_M_TOKEN)).hasRole(bytes32(0x00), _ADMIN)); + assertTrue(IAccessControl(address(_WRAPPED_M_TOKEN)).hasRole(keccak256("PAUSER_ROLE"), _PAUSER)); + assertTrue( + IAccessControl(address(_WRAPPED_M_TOKEN)).hasRole(keccak256("FREEZE_MANAGER_ROLE"), _FREEZE_MANAGER) + ); + assertTrue( + IAccessControl(address(_WRAPPED_M_TOKEN)).hasRole( + keccak256("FORCED_TRANSFER_MANAGER_ROLE"), + _FORCED_TRANSFER_MANAGER + ) + ); + assertTrue( + IAccessControl(address(_WRAPPED_M_TOKEN)).hasRole(keccak256("EXCESS_MANAGER_ROLE"), _EXCESS_MANAGER) + ); + + // Should not be able to call initialize again. + vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector)); + _WRAPPED_M_TOKEN.initialize( + _ADMIN, + _FREEZE_MANAGER, + _PAUSER, + _FORCED_TRANSFER_MANAGER, + _EXCESS_MANAGER, + _EXCESS_DESTINATION + ); + + // Relevant storage slots. + assertEq(_WRAPPED_M_TOKEN.totalEarningSupply(), totalEarningSupply_); + + for (uint256 index_; index_ < _earners.length; ++index_) { + assertEq(_WRAPPED_M_TOKEN.balanceWithYieldOf(_earners[index_]), balancesWithYield_[index_]); + } + } +} diff --git a/test/integration/vendor/morpho-blue/Interfaces.sol b/test/integration/vendor/morpho-blue/Interfaces.sol index 479a6e9..d023e22 100644 --- a/test/integration/vendor/morpho-blue/Interfaces.sol +++ b/test/integration/vendor/morpho-blue/Interfaces.sol @@ -1,15 +1,18 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.23; -interface IMorphoBlueFactory { - struct MarketParams { - address loanToken; - address collateralToken; - address oracle; - address irm; - uint256 lltv; - } +pragma solidity 0.8.26; +type Id is bytes32; + +struct MarketParams { + address loanToken; + address collateralToken; + address oracle; + address irm; + uint256 lltv; +} + +interface IMorphoBlueLike { function createMarket(MarketParams memory marketParams) external; function supply( @@ -96,3 +99,36 @@ interface IMorphoChainlinkOracleV2Factory { interface IOracle { function price() external view returns (uint256); } + +interface IMorphoVaultFactoryLike { + function createMetaMorpho( + address initialOwner, + uint256 initialTimelock, + address asset, + string memory name, + string memory symbol, + bytes32 salt + ) external returns (address vault); + + function isMetaMorpho(address vault) external view returns (bool); +} + +interface IMorphoVaultLike { + function setFee(uint256 newFee) external; + + function setFeeRecipient(address newFeeRecipient) external; + + function submitCap(MarketParams memory marketParams, uint256 newSupplyCap) external; + + function acceptCap(MarketParams memory marketParams) external; + + function setSupplyQueue(Id[] calldata newSupplyQueue) external; + + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + function balanceOf(address account) external view returns (uint256); + + function owner() external view returns (address); +} diff --git a/test/integration/vendor/morpho-blue/MorphoTestBase.sol b/test/integration/vendor/morpho-blue/MorphoTestBase.sol new file mode 100644 index 0000000..8357eb8 --- /dev/null +++ b/test/integration/vendor/morpho-blue/MorphoTestBase.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { IERC20 } from "../../../../lib/common/src/interfaces/IERC20.sol"; + +import { + Id, + MarketParams, + IMorphoBlueLike, + IMorphoChainlinkOracleV2Factory, + IMorphoVaultFactoryLike, + IMorphoVaultLike +} from "./Interfaces.sol"; + +import { TestBase } from "../../TestBase.sol"; + +contract MorphoTestBase is TestBase { + uint256 internal constant _MARKET_PARAMS_BYTES_LENGTH = 5 * 32; + + // Morpho Blue factory on Ethereum Mainnet + address internal constant _MORPHO = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb; + + // Morpho Vault factory on Ethereum Mainnet + address internal constant _MORPHO_VAULT_FACTORY = 0xA9c3D3a366466Fa809d1Ae982Fb2c46E5fC41101; + + // Oracle factory on Ethereum Mainnet + address internal constant _ORACLE_FACTORY = 0x3A7bB36Ee3f3eE32A60e9f2b33c1e5f2E83ad766; + + // Morpho Blue market Liquidation Loan-To-Value ratio + uint256 internal constant _LLTV = 94_5000000000000000; // 94.5% + + Id internal constant _IDLE_MARKET_ID = Id.wrap(0x7725318760d6d193e11f889f0be58eba134f64a8c22ed9050cac7bd4a70a64f0); + + address internal _oracle; + + /* ============ Oracles ============ */ + + function _createOracle() internal returns (address oracle_) { + return + IMorphoChainlinkOracleV2Factory(_ORACLE_FACTORY).createMorphoChainlinkOracleV2( + address(0), + 1, + address(0), + address(0), + 6, + address(0), + 1, + address(0), + address(0), + 6, + bytes32(0) + ); + } + + /* ============ Markets ============ */ + + function _createMarket( + address account_, + address loanToken_, + address collateralToken_, + address oracle_, + uint256 lltv_ + ) internal { + MarketParams memory marketParams_ = MarketParams({ + loanToken: loanToken_, + collateralToken: collateralToken_, + oracle: oracle_, + irm: address(0), + lltv: lltv_ + }); + + vm.prank(account_); + IMorphoBlueLike(_MORPHO).createMarket(marketParams_); + } + + function _createIdleMarket(address account_) internal { + _createMarket(account_, address(0), address(0), address(0), 0); + } + + function _supplyCollateral( + address account_, + address collateralToken_, + uint256 amount_, + address loanToken_ + ) internal { + _approve(collateralToken_, account_, _MORPHO, amount_); + + MarketParams memory marketParams_ = MarketParams({ + loanToken: loanToken_, + collateralToken: collateralToken_, + oracle: _oracle, + irm: address(0), + lltv: _LLTV + }); + + vm.prank(account_); + IMorphoBlueLike(_MORPHO).supplyCollateral(marketParams_, amount_, account_, hex""); + } + + function _withdrawCollateral( + address account_, + address collateralToken_, + uint256 amount_, + address receiver_, + address loanToken_ + ) internal { + MarketParams memory marketParams_ = MarketParams({ + loanToken: loanToken_, + collateralToken: collateralToken_, + oracle: _oracle, + irm: address(0), + lltv: _LLTV + }); + + vm.prank(account_); + IMorphoBlueLike(_MORPHO).withdrawCollateral(marketParams_, amount_, account_, receiver_); + } + + function _supply( + address account_, + address loanToken_, + uint256 amount_, + address collateralToken_ + ) internal returns (uint256 assetsSupplied_, uint256 sharesSupplied_) { + _approve(loanToken_, account_, _MORPHO, amount_); + + MarketParams memory marketParams_ = MarketParams({ + loanToken: loanToken_, + collateralToken: collateralToken_, + oracle: _oracle, + irm: address(0), + lltv: _LLTV + }); + + vm.prank(account_); + return IMorphoBlueLike(_MORPHO).supply(marketParams_, amount_, 0, account_, hex""); + } + + function _withdraw( + address account_, + address loanToken_, + uint256 amount_, + address receiver_, + address collateralToken_ + ) internal returns (uint256 assetsWithdrawn_, uint256 sharesWithdrawn_) { + MarketParams memory marketParams_ = MarketParams({ + loanToken: loanToken_, + collateralToken: collateralToken_, + oracle: _oracle, + irm: address(0), + lltv: _LLTV + }); + + vm.prank(account_); + return IMorphoBlueLike(_MORPHO).withdraw(marketParams_, amount_, 0, account_, receiver_); + } + + function _borrow( + address account_, + address loanToken_, + uint256 amount_, + address receiver_, + address collateralToken_ + ) internal returns (uint256 assetsBorrowed_, uint256 sharesBorrowed_) { + MarketParams memory marketParams_ = MarketParams({ + loanToken: loanToken_, + collateralToken: collateralToken_, + oracle: _oracle, + irm: address(0), + lltv: _LLTV + }); + + vm.prank(account_); + return IMorphoBlueLike(_MORPHO).borrow(marketParams_, amount_, 0, account_, receiver_); + } + + function _repay( + address account_, + address loanToken_, + uint256 amount_, + address collateralToken_ + ) internal returns (uint256 assetsRepaid_, uint256 sharesRepaid_) { + _approve(loanToken_, account_, _MORPHO, amount_); + + MarketParams memory marketParams_ = MarketParams({ + loanToken: loanToken_, + collateralToken: collateralToken_, + oracle: _oracle, + irm: address(0), + lltv: _LLTV + }); + + vm.prank(account_); + return IMorphoBlueLike(_MORPHO).repay(marketParams_, amount_, 0, account_, hex""); + } + + function _getMarketId(MarketParams memory marketParams_) internal pure returns (Id marketParamsId_) { + assembly ("memory-safe") { + marketParamsId_ := keccak256(marketParams_, _MARKET_PARAMS_BYTES_LENGTH) + } + } + + /* ============ ERC20 ============ */ + + function _approve(address token_, address account_, address spender_, uint256 amount_) internal { + vm.prank(account_); + IERC20(token_).approve(spender_, amount_); + } + + function _transfer(address token_, address sender_, address recipient_, uint256 amount_) internal { + vm.prank(sender_); + IERC20(token_).transfer(recipient_, amount_); + } +} diff --git a/test/integration/vendor/protocol/Interfaces.sol b/test/integration/vendor/protocol/Interfaces.sol index 857d5ae..f70dd17 100644 --- a/test/integration/vendor/protocol/Interfaces.sol +++ b/test/integration/vendor/protocol/Interfaces.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.23; +pragma solidity 0.8.26; interface IRegistrarLike { function addToList(bytes32 list, address account) external; @@ -24,4 +24,16 @@ interface IMTokenLike { function totalEarningSupply() external view returns (uint240 supply); function totalNonEarningSupply() external view returns (uint240 supply); + + function startEarning() external; +} + +interface ISwapFacilityLike { + function grantRole(bytes32 role, address account) external; + + function swapInM(address extensionOut, uint256 amount, address recipient) external; + + function swapOutM(address extensionIn, uint256 amount, address recipient) external; + + function setPermissionedMSwapper(address extension, address swapper, bool allowed) external; } diff --git a/test/integration/vendor/uniswap-v3/Interfaces.sol b/test/integration/vendor/uniswap-v3/Interfaces.sol index 176fdff..cc8dca0 100644 --- a/test/integration/vendor/uniswap-v3/Interfaces.sol +++ b/test/integration/vendor/uniswap-v3/Interfaces.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.26; interface INonfungiblePositionManager { struct MintParams { diff --git a/test/integration/vendor/uniswap-v3/Utils.sol b/test/integration/vendor/uniswap-v3/Utils.sol index b17becc..06bb2fd 100644 --- a/test/integration/vendor/uniswap-v3/Utils.sol +++ b/test/integration/vendor/uniswap-v3/Utils.sol @@ -1,13 +1,10 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; uint256 constant PRECISION = 2 ** 96; library Utils { - int24 internal constant MIN_TICK = -887272; - int24 internal constant MAX_TICK = -MIN_TICK; - function encodePriceSqrt(uint256 reserve1, uint256 reserve0) internal pure returns (uint160) { return uint160(sqrt((reserve1 * PRECISION * PRECISION) / reserve0)); } diff --git a/test/unit/ListOfEarnersToMigrate.t.sol b/test/unit/ListOfEarnersToMigrate.t.sol new file mode 100644 index 0000000..9c474f2 --- /dev/null +++ b/test/unit/ListOfEarnersToMigrate.t.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +import { Test } from "../../lib/forge-std/src/Test.sol"; + +import { ListOfEarnersToMigrate } from "../../src/ListOfEarnersToMigrate.sol"; + +import { EarnersAddresses } from "../../script/EarnersAddresses.sol"; + +contract ListOfEarnersToMigrateTests is Test { + function test_constructor_empty() external { + ListOfEarnersToMigrate list_ = new ListOfEarnersToMigrate(new address[](0)); + + assertEq(list_.getEarners().length, 0); + } + + function test_constructor_single() external { + address[] memory earners_ = new address[](1); + earners_[0] = address(1); + + ListOfEarnersToMigrate list_ = new ListOfEarnersToMigrate(earners_); + + address[] memory stored_ = list_.getEarners(); + + assertEq(stored_.length, 1); + assertEq(stored_[0], address(1)); + } + + function test_constructor_sortedAscending() external { + address[] memory earners_ = new address[](3); + earners_[0] = address(1); + earners_[1] = address(2); + earners_[2] = address(3); + + ListOfEarnersToMigrate list_ = new ListOfEarnersToMigrate(earners_); + + address[] memory stored_ = list_.getEarners(); + + assertEq(stored_.length, 3); + assertEq(stored_[0], address(1)); + assertEq(stored_[1], address(2)); + assertEq(stored_[2], address(3)); + } + + function test_constructor_revertsOnUnsorted() external { + address[] memory earners_ = new address[](3); + earners_[0] = address(1); + earners_[1] = address(3); + earners_[2] = address(2); + + vm.expectRevert(ListOfEarnersToMigrate.EarnersNotSortedOrUnique.selector); + new ListOfEarnersToMigrate(earners_); + } + + function test_constructor_revertsOnDuplicate() external { + address[] memory earners_ = new address[](3); + earners_[0] = address(1); + earners_[1] = address(2); + earners_[2] = address(2); + + vm.expectRevert(ListOfEarnersToMigrate.EarnersNotSortedOrUnique.selector); + new ListOfEarnersToMigrate(earners_); + } + + function test_constructor_revertsOnZeroAddress() external { + address[] memory earners_ = new address[](2); + earners_[0] = address(0); + earners_[1] = address(1); + + vm.expectRevert(ListOfEarnersToMigrate.EarnersNotSortedOrUnique.selector); + new ListOfEarnersToMigrate(earners_); + } + + function test_constructor_committedEarnersAreDeployable() external { + _assertDeployable(EarnersAddresses.getEthereumEarners()); + _assertDeployable(EarnersAddresses.getArbitrumEarners()); + _assertDeployable(EarnersAddresses.getPlumeEarners()); + } + + function _assertDeployable(address[] memory earners_) internal { + address[] memory stored_ = new ListOfEarnersToMigrate(earners_).getEarners(); + + assertEq(stored_.length, earners_.length); + + for (uint256 i_ = 1; i_ < stored_.length; ++i_) { + assertTrue(uint160(stored_[i_ - 1]) < uint160(stored_[i_])); + } + } +} diff --git a/test/unit/Migrations.t.sol b/test/unit/Migrations.t.sol new file mode 100644 index 0000000..04787f3 --- /dev/null +++ b/test/unit/Migrations.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +import { Proxy } from "../../lib/common/src/Proxy.sol"; +import { Test } from "../../lib/forge-std/src/Test.sol"; + +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; + +import { WrappedMToken } from "../../src/WrappedMToken.sol"; +import { WrappedMTokenMigratorV1 as WrappedMTokenMigrator } from "../../src/WrappedMTokenMigratorV1.sol"; + +import { MockRegistrar } from "./../utils/Mocks.sol"; + +contract Foo { + address public admin; + address public freezeManager; + address public pauser; + address public forcedTransferManager; + address public excessManager; + address public excessDestination; + + function bar() external pure returns (uint256) { + return 1; + } + + function initialize( + address admin_, + address freezeManager_, + address pauser_, + address forcedTransferManager_, + address excessManager_, + address excessDestination_ + ) public { + admin = admin_; + freezeManager = freezeManager_; + pauser = pauser_; + forcedTransferManager = forcedTransferManager_; + excessManager = excessManager_; + excessDestination = excessDestination_; + } +} + +contract MigrationTests is Test { + bytes32 internal constant _WM_MIGRATOR_KEY_PREFIX = "wm_migrator_v2"; + bytes32 internal constant _EM_MIGRATOR_KEY_PREFIX = "em_migrator_v1"; + + address internal _alice = makeAddr("alice"); + address internal _bob = makeAddr("bob"); + address internal _carol = makeAddr("carol"); + address internal _dave = makeAddr("dave"); + + address internal _mToken = makeAddr("mToken"); + address internal _swapFacility = makeAddr("swapFacility"); + + address internal _admin = makeAddr("admin"); + address internal _excessManager = makeAddr("excessManager"); + address internal _excessDestination = makeAddr("excessDestination"); + address internal _freezeManager = makeAddr("freezeManager"); + address internal _forcedTransferManager = makeAddr("forcedTransferManager"); + address internal _migrationAdmin = makeAddr("migrationAdmin"); + address internal _pauser = makeAddr("pauser"); + + function test_wrappedMToken_migration() external { + MockRegistrar registrar_ = new MockRegistrar(); + address mToken_ = makeAddr("mToken"); + + address implementation_ = address( + new WrappedMToken(address(mToken_), address(registrar_), _swapFacility, _migrationAdmin) + ); + + address proxy_ = address(new Proxy(address(implementation_))); + address migrator_ = address( + new WrappedMTokenMigrator( + address(new Foo()), + new address[](0), + _admin, + _freezeManager, + _pauser, + _forcedTransferManager, + _excessManager, + _excessDestination + ) + ); + + registrar_.set(keccak256(abi.encode(_WM_MIGRATOR_KEY_PREFIX, proxy_)), bytes32(uint256(uint160(migrator_)))); + + vm.expectRevert(); + Foo(proxy_).bar(); + + IWrappedMToken(proxy_).migrate(); + + assertEq(Foo(proxy_).bar(), 1); + assertEq(Foo(proxy_).admin(), _admin); + assertEq(Foo(proxy_).freezeManager(), _freezeManager); + assertEq(Foo(proxy_).pauser(), _pauser); + assertEq(Foo(proxy_).forcedTransferManager(), _forcedTransferManager); + assertEq(Foo(proxy_).excessManager(), _excessManager); + assertEq(Foo(proxy_).excessDestination(), _excessDestination); + } + + function test_wrappedMToken_migration_fromAdmin() external { + MockRegistrar registrar_ = new MockRegistrar(); + address mToken_ = makeAddr("mToken"); + + address implementation_ = address( + new WrappedMToken(address(mToken_), address(registrar_), _swapFacility, _migrationAdmin) + ); + + address proxy_ = address(new Proxy(address(implementation_))); + address migrator_ = address( + new WrappedMTokenMigrator( + address(new Foo()), + new address[](0), + _admin, + _freezeManager, + _pauser, + _forcedTransferManager, + _excessManager, + _excessDestination + ) + ); + + vm.expectRevert(); + Foo(proxy_).bar(); + + vm.prank(_migrationAdmin); + IWrappedMToken(proxy_).migrate(migrator_); + + assertEq(Foo(proxy_).bar(), 1); + assertEq(Foo(proxy_).admin(), _admin); + assertEq(Foo(proxy_).freezeManager(), _freezeManager); + assertEq(Foo(proxy_).pauser(), _pauser); + assertEq(Foo(proxy_).forcedTransferManager(), _forcedTransferManager); + assertEq(Foo(proxy_).excessManager(), _excessManager); + assertEq(Foo(proxy_).excessDestination(), _excessDestination); + } +} diff --git a/test/Stories.t.sol b/test/unit/Stories.t.sol similarity index 55% rename from test/Stories.t.sol rename to test/unit/Stories.t.sol index 720daf0..1511518 100644 --- a/test/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -1,53 +1,72 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; -import { Test } from "../lib/forge-std/src/Test.sol"; +import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; -import { IWrappedMToken } from "../src/interfaces/IWrappedMToken.sol"; +import { Proxy } from "../../lib/common/src/Proxy.sol"; +import { Test } from "../../lib/forge-std/src/Test.sol"; -import { WrappedMToken } from "../src/WrappedMToken.sol"; -import { Proxy } from "../src/Proxy.sol"; +import { ISwapFacilityLike } from "../../src/interfaces/ISwapFacilityLike.sol"; +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { MockM, MockRegistrar } from "./utils/Mocks.sol"; +import { WrappedMToken } from "../../src/WrappedMToken.sol"; -contract Tests is Test { - uint56 internal constant _EXP_SCALED_ONE = 1e12; +import { MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; - bytes32 internal constant _EARNERS_LIST = "earners"; - bytes32 internal constant _MIGRATOR_V1_PREFIX = "wm_migrator_v1"; +contract StoryTests is Test { + uint56 internal constant _EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; + + bytes32 internal constant _EARNERS_LIST_NAME = "earners"; address internal _alice = makeAddr("alice"); address internal _bob = makeAddr("bob"); address internal _carol = makeAddr("carol"); address internal _dave = makeAddr("dave"); + address internal _admin = makeAddr("admin"); + address internal _excessManager = makeAddr("excessManager"); + address internal _excessDestination = makeAddr("excessDestination"); + address internal _forcedTransferManager = makeAddr("forcedTransferManager"); + address internal _freezeManager = makeAddr("freezeManager"); address internal _migrationAdmin = makeAddr("migrationAdmin"); - - address internal _vault = makeAddr("vault"); + address internal _pauser = makeAddr("pauser"); MockM internal _mToken; MockRegistrar internal _registrar; + MockSwapFacility internal _swapFacility; WrappedMToken internal _implementation; IWrappedMToken internal _wrappedMToken; function setUp() external { _registrar = new MockRegistrar(); - _registrar.setVault(_vault); _mToken = new MockM(); _mToken.setCurrentIndex(_EXP_SCALED_ONE); - _mToken.setTtgRegistrar(address(_registrar)); + _swapFacility = new MockSwapFacility(address(_mToken)); - _implementation = new WrappedMToken(address(_mToken), _migrationAdmin); + _implementation = new WrappedMToken( + address(_mToken), + address(_registrar), + address(_swapFacility), + _migrationAdmin + ); _wrappedMToken = IWrappedMToken(address(new Proxy(address(_implementation)))); + WrappedMToken(address(_wrappedMToken)).initialize( + _admin, + _freezeManager, + _pauser, + _forcedTransferManager, + _excessManager, + _excessDestination + ); } function test_story() external { - _registrar.setListContains(_EARNERS_LIST, _alice, true); - _registrar.setListContains(_EARNERS_LIST, _bob, true); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -57,8 +76,14 @@ contract Tests is Test { _mToken.setBalanceOf(_alice, 100_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + vm.prank(_alice); - _wrappedMToken.wrap(_alice, 100_000000); + _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _alice); // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), 100_000000); @@ -73,8 +98,14 @@ contract Tests is Test { _mToken.setBalanceOf(_carol, 100_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_carol) + ); + vm.prank(_carol); - _wrappedMToken.wrap(_carol, 100_000000); + _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _carol); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), 100_000000); @@ -107,8 +138,14 @@ contract Tests is Test { _mToken.setBalanceOf(_bob, 100_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_bob) + ); + vm.prank(_bob); - _wrappedMToken.wrap(_bob, 100_000000); + _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _bob); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), 100_000000); @@ -124,8 +161,14 @@ contract Tests is Test { _mToken.setBalanceOf(_dave, 100_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_dave) + ); + vm.prank(_dave); - _wrappedMToken.wrap(_dave, 100_000000); + _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _dave); // Assert Dave (Non-Earner) assertEq(_wrappedMToken.balanceOf(_dave), 100_000000); @@ -179,54 +222,54 @@ contract Tests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 200_000000); assertEq(_wrappedMToken.totalSupply(), 500_000000); assertEq(_wrappedMToken.totalAccruedYield(), 150_000000); - assertEq(_wrappedMToken.excess(), 249_999999); + assertEq(_wrappedMToken.excess(), 250_000000); vm.prank(_alice); _wrappedMToken.transfer(_carol, 100_000000); // Assert Alice (Earner) - assertEq(_wrappedMToken.balanceOf(_alice), 200_000000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 100_000000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99_999998); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), 200_000000); assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 300_000000); + assertEq(_wrappedMToken.totalEarningSupply(), 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 300_000000); - assertEq(_wrappedMToken.totalSupply(), 600_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 50_000001); + assertEq(_wrappedMToken.totalSupply(), 500_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 150_000001); assertEq(_wrappedMToken.excess(), 249_999999); vm.prank(_dave); _wrappedMToken.transfer(_bob, 50_000000); // Assert Bob (Earner) - assertEq(_wrappedMToken.balanceOf(_bob), 200_000000); - assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); + assertEq(_wrappedMToken.balanceOf(_bob), 150_000000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 49_999998); // Assert Dave (Non-Earner) assertEq(_wrappedMToken.balanceOf(_dave), 50_000000); assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); + assertEq(_wrappedMToken.totalEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 250_000000); - assertEq(_wrappedMToken.totalSupply(), 650_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 2); - assertEq(_wrappedMToken.excess(), 249_999996); + assertEq(_wrappedMToken.totalSupply(), 500_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 150_000002); + assertEq(_wrappedMToken.excess(), 249_999998); _mToken.setCurrentIndex(4 * _EXP_SCALED_ONE); _mToken.setBalanceOf(address(_wrappedMToken), 1_200_000000); // was 900 @ 3.0, so 1200 @ 4.0 // Assert Alice (Earner) - assertEq(_wrappedMToken.balanceOf(_alice), 200_000000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 66_666664); + assertEq(_wrappedMToken.balanceOf(_alice), 100_000000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 166_666664); // Assert Bob (Earner) - assertEq(_wrappedMToken.balanceOf(_bob), 200_000000); - assertEq(_wrappedMToken.accruedYieldOf(_bob), 66_666664); + assertEq(_wrappedMToken.balanceOf(_bob), 150_000000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 116_666664); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), 200_000000); @@ -237,13 +280,13 @@ contract Tests is Test { assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); + assertEq(_wrappedMToken.totalEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 250_000000); - assertEq(_wrappedMToken.totalSupply(), 650_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 133_333336); + assertEq(_wrappedMToken.totalSupply(), 500_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 283_333336); assertEq(_wrappedMToken.excess(), 416_666664); - _registrar.setListContains(_EARNERS_LIST, _alice, false); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, false); _wrappedMToken.stopEarningFor(_alice); @@ -252,13 +295,13 @@ contract Tests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 200_000000); + assertEq(_wrappedMToken.totalEarningSupply(), 150_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 516_666664); - assertEq(_wrappedMToken.totalSupply(), 716_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 66_666672); + assertEq(_wrappedMToken.totalSupply(), 666_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 116_666672); assertEq(_wrappedMToken.excess(), 416_666664); - _registrar.setListContains(_EARNERS_LIST, _carol, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); _wrappedMToken.startEarningFor(_carol); @@ -267,10 +310,10 @@ contract Tests is Test { assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); + assertEq(_wrappedMToken.totalEarningSupply(), 350_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 316_666664); - assertEq(_wrappedMToken.totalSupply(), 716_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 66_666672); + assertEq(_wrappedMToken.totalSupply(), 666_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 116_666672); assertEq(_wrappedMToken.excess(), 416_666664); _mToken.setCurrentIndex(5 * _EXP_SCALED_ONE); @@ -281,8 +324,8 @@ contract Tests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); // Assert Bob (Earner) - assertEq(_wrappedMToken.balanceOf(_bob), 200_000000); - assertEq(_wrappedMToken.accruedYieldOf(_bob), 133_333330); + assertEq(_wrappedMToken.balanceOf(_bob), 150_000000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 183_333330); // Assert Carol (Earner) assertEq(_wrappedMToken.balanceOf(_carol), 200_000000); @@ -293,56 +336,92 @@ contract Tests is Test { assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); + assertEq(_wrappedMToken.totalEarningSupply(), 350_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 316_666664); - assertEq(_wrappedMToken.totalSupply(), 716_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 183_333340); - assertEq(_wrappedMToken.excess(), 599_999995); + assertEq(_wrappedMToken.totalSupply(), 666_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); + + vm.prank(_alice); + _wrappedMToken.approve(address(_swapFacility), 266_666664); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); vm.prank(_alice); - _wrappedMToken.unwrap(_alice, 266_666664); + _swapFacility.swapOutM(address(_wrappedMToken), 266_666664, _alice); // Assert Alice (Non-Earner) assertEq(_wrappedMToken.balanceOf(_alice), 0); assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); + assertEq(_wrappedMToken.totalEarningSupply(), 350_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); - assertEq(_wrappedMToken.totalSupply(), 450_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 183_333340); - assertEq(_wrappedMToken.excess(), 600_000000); + assertEq(_wrappedMToken.totalSupply(), 400_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_bob); - _wrappedMToken.unwrap(_bob, 333_333330); + _wrappedMToken.approve(address(_swapFacility), 150_000000); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_bob) + ); + + vm.prank(_bob); + _swapFacility.swapOutM(address(_wrappedMToken), 150_000000, _bob); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), 0); - assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 183_333330); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 250_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 50_000010); - assertEq(_wrappedMToken.excess(), 600_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); + + vm.prank(_carol); + _wrappedMToken.approve(address(_swapFacility), 200_000000); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_carol) + ); vm.prank(_carol); - _wrappedMToken.unwrap(_carol, 250_000000); + _swapFacility.swapOutM(address(_wrappedMToken), 200_000000, _carol); // Assert Carol (Earner) assertEq(_wrappedMToken.balanceOf(_carol), 0); - assertEq(_wrappedMToken.accruedYieldOf(_carol), 0); + assertEq(_wrappedMToken.accruedYieldOf(_carol), 50_000000); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 50_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), 600_000010); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); + + vm.prank(_dave); + _wrappedMToken.approve(address(_swapFacility), 50_000000); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_dave) + ); vm.prank(_dave); - _wrappedMToken.unwrap(_dave, 50_000000); + _swapFacility.swapOutM(address(_wrappedMToken), 50_000000, _dave); // Assert Dave (Non-Earner) assertEq(_wrappedMToken.balanceOf(_dave), 0); @@ -352,23 +431,15 @@ contract Tests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalSupply(), 0); - assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), 600_000010); - - _wrappedMToken.claimExcess(); - - // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalSupply(), 0); - assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); } function test_noExcessCreep() external { - _registrar.setListContains(_EARNERS_LIST, _alice, true); - _registrar.setListContains(_EARNERS_LIST, _bob, true); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(_EXP_SCALED_ONE + 3e11 - 1); _wrappedMToken.enableEarning(); @@ -377,32 +448,55 @@ contract Tests is Test { _mToken.setBalanceOf(_alice, 1_000000); for (uint256 i_; i_ < 100; ++i_) { + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + vm.prank(_alice); - _wrappedMToken.wrap(_alice, 9); + _swapFacility.swapInM(address(_wrappedMToken), 9, _alice); assertLe( - _wrappedMToken.balanceOf(_alice) + _wrappedMToken.excess(), - _mToken.balanceOf(address(_wrappedMToken)) + int256(_wrappedMToken.balanceOf(_alice)) + int256(_wrappedMToken.excess()), + int256(_mToken.balanceOf(address(_wrappedMToken))) ); } - _wrappedMToken.claimExcess(); + assertEq(_wrappedMToken.excess(), 0); uint256 aliceBalance_ = _wrappedMToken.balanceOf(_alice); vm.prank(_alice); _wrappedMToken.transfer(_bob, aliceBalance_); - assertLe(_wrappedMToken.balanceOf(_bob) + _wrappedMToken.excess(), _mToken.balanceOf(address(_wrappedMToken))); + assertLe( + int256(_wrappedMToken.balanceOf(_bob)) + + int256(_wrappedMToken.excess()) + + int256(_wrappedMToken.balanceOf(_excessDestination)), + int256(_mToken.balanceOf(address(_wrappedMToken))) + ); + + uint256 bobBalance_ = _wrappedMToken.balanceOf(_bob); + + vm.prank(_bob); + _wrappedMToken.approve(address(_swapFacility), bobBalance_); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_bob) + ); vm.prank(_bob); - _wrappedMToken.unwrap(_bob); + _swapFacility.swapOutM(address(_wrappedMToken), bobBalance_, _bob); } function test_dustWrapping() external { - _registrar.setListContains(_EARNERS_LIST, _alice, true); - _registrar.setListContains(_EARNERS_LIST, _bob, true); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(_EXP_SCALED_ONE + 1); _wrappedMToken.enableEarning(); @@ -411,12 +505,18 @@ contract Tests is Test { _mToken.setBalanceOf(_alice, 1_000000); for (uint256 i_; i_ < 100; ++i_) { + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + vm.prank(_alice); - _wrappedMToken.wrap(_alice, 1); + _swapFacility.swapInM(address(_wrappedMToken), 1, _alice); assertLe( - _wrappedMToken.balanceOf(_alice) + _wrappedMToken.excess(), - _mToken.balanceOf(address(_wrappedMToken)) + int256(_wrappedMToken.balanceOf(_alice)) + int256(_wrappedMToken.excess()), + int256(_mToken.balanceOf(address(_wrappedMToken))) ); } diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol new file mode 100644 index 0000000..44a6fe8 --- /dev/null +++ b/test/unit/WrappedMToken.t.sol @@ -0,0 +1,2709 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { + IAccessControl +} from "../../../lib/common/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/access/IAccessControl.sol"; + +import { + PausableUpgradeable +} from "../../../lib/common/lib/openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol"; + +import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; +import { UIntMath } from "../../lib/common/src/libs/UIntMath.sol"; + +import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; +import { IERC20Extended } from "../../lib/common/src/interfaces/IERC20Extended.sol"; + +import { Proxy } from "../../lib/common/src/Proxy.sol"; + +import { + IForcedTransferable +} from "../../lib/evm-m-extensions/src/components/forcedTransferable/IForcedTransferable.sol"; +import { IFreezable } from "../../lib/evm-m-extensions/src/components/freezable/IFreezable.sol"; +import { IPausable } from "../../lib/evm-m-extensions/src/components/pausable/IPausable.sol"; + +import { ISwapFacilityLike } from "../../src/interfaces/ISwapFacilityLike.sol"; +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; + +import { WrappedMTokenHarness } from "../harness/WrappedMTokenHarness.sol"; + +import { BaseUnitTest } from "../utils/BaseUnitTest.sol"; +import { MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; + +// TODO: All operations involving earners should include demonstration of accrued yield being added to their balance. +// TODO: Add relevant unit tests while earning enabled/disabled. +contract WrappedMTokenTests is BaseUnitTest { + WrappedMTokenHarness internal _implementation; + + function setUp() external { + _registrar = new MockRegistrar(); + + _mToken = new MockM(); + + _swapFacility = new MockSwapFacility(address(_mToken)); + + _implementation = new WrappedMTokenHarness( + address(_mToken), + address(_registrar), + address(_swapFacility), + _migrationAdmin + ); + + _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + _wrappedMToken.initialize( + _admin, + _freezeManager, + _pauser, + _forcedTransferManager, + _excessManager, + _excessDestination + ); + } + + /* ============ constants ============ */ + function test_constants() external view { + assertEq(_wrappedMToken.EARNERS_LIST_IGNORED_KEY(), "earners_list_ignored"); + assertEq(_wrappedMToken.EARNERS_LIST_NAME(), _EARNERS_LIST_NAME); + assertEq(_wrappedMToken.CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX(), _CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX); + assertEq(_wrappedMToken.MIGRATOR_KEY_PREFIX(), "wm_migrator_v2"); + } + + /* ============ constructor ============ */ + function test_constructor() external view { + assertEq(_wrappedMToken.migrationAdmin(), _migrationAdmin); + assertEq(_wrappedMToken.mToken(), address(_mToken)); + assertEq(_wrappedMToken.registrar(), address(_registrar)); + assertEq(_wrappedMToken.excessDestination(), _excessDestination); + assertEq(_wrappedMToken.swapFacility(), address(_swapFacility)); + assertEq(_wrappedMToken.name(), "M (Wrapped) by M0"); + assertEq(_wrappedMToken.symbol(), "wM"); + assertEq(_wrappedMToken.decimals(), 6); + assertEq(_wrappedMToken.implementation(), address(_implementation)); + assertEq(_wrappedMToken.enableMIndex(), 0); + assertEq(_wrappedMToken.disableIndex(), 0); + } + + function test_constructor_zeroMToken() external { + vm.expectRevert(IWrappedMToken.ZeroMToken.selector); + new WrappedMTokenHarness(address(0), address(0), address(0), address(0)); + } + + function test_constructor_zeroRegistrar() external { + vm.expectRevert(IWrappedMToken.ZeroRegistrar.selector); + new WrappedMTokenHarness(address(_mToken), address(0), address(0), address(0)); + } + + function test_constructor_zeroSwapFacility() external { + vm.expectRevert(IWrappedMToken.ZeroSwapFacility.selector); + new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0)); + } + + function test_constructor_zeroMigrationAdmin() external { + vm.expectRevert(IWrappedMToken.ZeroMigrationAdmin.selector); + new WrappedMTokenHarness(address(_mToken), address(_registrar), address(_swapFacility), address(0)); + } + + function test_constructor_zeroImplementation() external { + vm.expectRevert(); + WrappedMTokenHarness(address(new Proxy(address(0)))); + } + + /* ============ initialize ============ */ + + function test_initialize() external view { + assertTrue(IAccessControl(address(_wrappedMToken)).hasRole(bytes32(0x00), _admin)); + assertTrue(IAccessControl(address(_wrappedMToken)).hasRole(_FREEZE_MANAGER_ROLE, _freezeManager)); + assertTrue(IAccessControl(address(_wrappedMToken)).hasRole(_PAUSER_ROLE, _pauser)); + assertTrue( + IAccessControl(address(_wrappedMToken)).hasRole(_FORCED_TRANSFER_MANAGER_ROLE, _forcedTransferManager) + ); + assertTrue(IAccessControl(address(_wrappedMToken)).hasRole(_EXCESS_MANAGER_ROLE, _excessManager)); + assertEq(_wrappedMToken.excessDestination(), _excessDestination); + } + + function test_initialize_zeroAdmin() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IWrappedMToken.ZeroAdmin.selector); + wrappedMToken_.initialize( + address(0), + _freezeManager, + _pauser, + _forcedTransferManager, + _excessManager, + _excessDestination + ); + } + + function test_initialize_zeroFreezeManager() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IFreezable.ZeroFreezeManager.selector); + wrappedMToken_.initialize( + _admin, + address(0), + _pauser, + _forcedTransferManager, + _excessManager, + _excessDestination + ); + } + + function test_initialize_zeroPauser() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IPausable.ZeroPauser.selector); + wrappedMToken_.initialize( + _admin, + _freezeManager, + address(0), + _forcedTransferManager, + _excessManager, + _excessDestination + ); + } + + function test_initialize_zeroForcedTransferManager() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IForcedTransferable.ZeroForcedTransferManager.selector); + wrappedMToken_.initialize(_admin, _freezeManager, _pauser, address(0), _excessManager, _excessDestination); + } + + function test_initialize_zeroExcessManager() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IWrappedMToken.ZeroExcessManager.selector); + wrappedMToken_.initialize( + _admin, + _freezeManager, + _pauser, + _forcedTransferManager, + address(0), + _excessDestination + ); + } + + function test_initialize_zeroExcessDestination() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IWrappedMToken.ZeroExcessDestination.selector); + wrappedMToken_.initialize(_admin, _freezeManager, _pauser, _forcedTransferManager, _excessManager, address(0)); + } + + /* ============ setExcessDestination ============ */ + + function test_setExcessDestination_notExcessManager() external { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + _alice, + _EXCESS_MANAGER_ROLE + ) + ); + + vm.prank(_alice); + _wrappedMToken.setExcessDestination(_bob); + } + + function test_setExcessDestination_zero() external { + vm.expectRevert(IWrappedMToken.ZeroExcessDestination.selector); + + vm.prank(_excessManager); + _wrappedMToken.setExcessDestination(address(0)); + } + + function test_setExcessDestination() external { + vm.expectEmit(); + emit IWrappedMToken.ExcessDestinationSet(_bob); + + vm.prank(_excessManager); + _wrappedMToken.setExcessDestination(_bob); + + assertEq(_wrappedMToken.excessDestination(), _bob); + } + + /* ============ _approve ============ */ + + function test_approve_frozenAccount() public { + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + + vm.prank(_alice); + _wrappedMToken.approve(_bob, 1_000); + } + + function test_approve_frozenSpender() public { + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _bob)); + + vm.prank(_alice); + _wrappedMToken.approve(_bob, 1_000); + } + + /* ============ _wrap ============ */ + function test_internalWrap_enforcedPause() external { + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, _alice, 1_000); + } + + function test_internalWrap_frozenAccount() external { + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, _bob, 1_000); + } + + function test_internalWrap_frozenRecipient() external { + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _bob)); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, _bob, 1_000); + } + + function test_internalWrap_insufficientAmount() external { + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, _alice, 0); + } + + function test_internalWrap_invalidRecipient() external { + _mToken.setBalanceOf(_alice, 1_000); + + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, address(0), 1_000); + } + + function test_internalWrap_toNonEarner() external { + _mToken.setBalanceOf(_alice, 1_000); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 1_000); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, _alice, 1_000); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 2_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 2_000); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + } + + function test_internalWrap_toEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _mToken.setBalanceOf(_alice, 1_002); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 999); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, _alice, 999); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 909); // round up total earning principal + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999); + assertEq(_wrappedMToken.totalAccruedYield(), 101); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 1); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, _alice, 1); + + // No change due to principal round down on wrap. + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999 + 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 98); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908 + 2); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999 + 1); + assertEq(_wrappedMToken.totalAccruedYield(), 101); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 2); + + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, _alice, 2); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0 + 1); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999 + 1 + 2); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 97); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908 + 0 + 4); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999 + 1 + 2); + assertEq(_wrappedMToken.totalAccruedYield(), 102); + } + + /* ============ wrap ============ */ + function test_wrap_notSwapFacility() external { + vm.expectRevert(IWrappedMToken.NotSwapFacility.selector); + + vm.prank(_alice); + _wrappedMToken.wrap(_alice, 1_000); + } + + function test_wrap_frozenAccount() external { + _mToken.setBalanceOf(_alice, 1_000); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + + vm.prank(_alice); + _swapFacility.swapInM(address(_wrappedMToken), 1_000, _alice); + } + + function test_wrap_frozenRecipient() external { + _mToken.setBalanceOf(_alice, 1_000); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _bob)); + + vm.prank(_alice); + _swapFacility.swapInM(address(_wrappedMToken), 1_000, _bob); + } + + function test_wrap_invalidAmount() external { + _mToken.setBalanceOf(_alice, uint256(type(uint240).max) + 1); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + + vm.expectRevert(UIntMath.InvalidUInt240.selector); + + vm.prank(_alice); + _swapFacility.swapInM(address(_wrappedMToken), uint256(type(uint240).max) + 1, _alice); + } + + function testFuzz_wrap( + bool earningEnabled_, + bool accountEarning_, + uint240 balanceWithYield_, + uint240 balance_, + uint240 wrapAmount_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); + + _setupAccount(_alice, earningEnabled_ && accountEarning_, balanceWithYield_, balance_); + + wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(_wrappedMToken.currentIndex()) - balanceWithYield_)); + + _mToken.setBalanceOf(_alice, wrapAmount_); + + if (wrapAmount_ == 0) { + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); + } else { + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, wrapAmount_); + } + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + + vm.startPrank(_alice); + _swapFacility.swapInM(address(_wrappedMToken), wrapAmount_, _alice); + + if (wrapAmount_ == 0) return; + + assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); + + assertEq( + earningEnabled_ && accountEarning_ + ? _wrappedMToken.totalEarningSupply() + : _wrappedMToken.totalNonEarningSupply(), + _wrappedMToken.balanceOf(_alice) + ); + } + + /* ============ wrap entire balance ============ */ + function test_wrap_entireBalance_invalidAmount() external { + _mToken.setBalanceOf(_alice, uint256(type(uint240).max) + 1); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + + vm.expectRevert(UIntMath.InvalidUInt240.selector); + + vm.prank(_alice); + _swapFacility.swapInM(address(_wrappedMToken), uint256(type(uint240).max) + 1, _alice); + } + + /* ============ _unwrap ============ */ + function test_internalUnwrap_enforcedPause() external { + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 1_000); + } + + function test_internal_unwrap_frozenAccount() external { + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 1_000); + } + + function test_internalUnwrap_insufficientAmount() external { + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 0); + } + + function test_internalUnwrap_insufficientBalance_fromNonEarner() external { + _wrappedMToken.setAccountOf(_alice, 999); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 1_000); + } + + function test_internalUnwrap_insufficientBalance_fromEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setAccountOf(_alice, 999, 909, false); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 1_000); + } + + function test_internalUnwrap_fromNonEarner() external { + _mToken.setIsEarning(address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _mToken.setBalanceOf(address(_wrappedMToken), 1_000); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 1); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 1); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 999); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 999); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 499); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 499); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 500); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 500); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 500); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 0); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + } + + function test_internalUnwrap_fromEarner() external { + _mToken.setIsEarning(address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _mToken.setBalanceOf(address(_wrappedMToken), 1_000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 100); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 1); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 1); + + // Change due to principal round up on unwrap. + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 - 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1); + assertEq(_wrappedMToken.totalAccruedYield(), 101); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 499); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 499); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1 - 454); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 - 1 - 499); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 - 1 - 454 + 2); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1 - 499); + assertEq(_wrappedMToken.totalAccruedYield(), 102); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 500); + + vm.prank(_alice); + _wrappedMToken.internalUnwrap(_alice, 500); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1 - 454 - 455); // 0 + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 - 1 - 499 - 500); // 0 + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 - 1 - 454 - 455 + 3); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1 - 499 - 500); // 0 + assertEq(_wrappedMToken.totalAccruedYield(), 103); + } + + /* ============ unwrap ============ */ + function test_unwrap_notSwapFacility() external { + vm.expectRevert(IWrappedMToken.NotSwapFacility.selector); + + vm.prank(_alice); + _wrappedMToken.unwrap(_alice, 1_000); + } + + function test_unwrap_frozenAccount() external { + uint256 amount_ = 1_000; + _wrappedMToken.setAccountOf(_alice, amount_); + + vm.prank(_alice); + _wrappedMToken.approve(address(_swapFacility), amount_); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + + vm.prank(_alice); + _swapFacility.swapOutM(address(_wrappedMToken), amount_, _alice); + } + + function test_unwrap_invalidAmount() external { + vm.prank(_alice); + _wrappedMToken.approve(address(_swapFacility), uint256(type(uint240).max) + 1); + + vm.expectRevert(UIntMath.InvalidUInt240.selector); + + vm.prank(_alice); + _swapFacility.swapOutM(address(_wrappedMToken), uint256(type(uint240).max) + 1, _alice); + } + + function testFuzz_unwrap( + bool earningEnabled_, + bool accountEarning_, + uint240 balanceWithYield_, + uint240 balance_, + uint240 unwrapAmount_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); + + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); + + _mToken.setBalanceOf(address(_wrappedMToken), balance_); + + unwrapAmount_ = uint240(bound(unwrapAmount_, 0, (11 * balance_) / 10)); + + vm.startPrank(_alice); + _wrappedMToken.approve(address(_swapFacility), unwrapAmount_); + + if (unwrapAmount_ == 0) { + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); + } else if (unwrapAmount_ > balance_) { + vm.expectRevert( + abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, balance_, unwrapAmount_) + ); + } else { + vm.expectEmit(); + emit IERC20.Transfer(address(_swapFacility), address(0), unwrapAmount_); + } + + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + + vm.startPrank(_alice); + _swapFacility.swapOutM(address(_wrappedMToken), unwrapAmount_, _alice); + + if ((unwrapAmount_ == 0) || (unwrapAmount_ > balance_)) return; + + assertEq(_wrappedMToken.balanceOf(_alice), balance_ - unwrapAmount_); + + assertEq( + accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), + _wrappedMToken.balanceOf(_alice) + ); + } + + /* ============ claimFor ============ */ + function test_claimFor_enforcedPause() external { + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + _wrappedMToken.claimFor(_alice); + } + + function test_claimFor_frozenAccount() external { + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + + vm.prank(_alice); + _wrappedMToken.claimFor(_alice); + } + + function test_claimFor_frozenClaimRecipient() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, true); // 1_100 balance with yield. + _wrappedMToken.setInternalClaimRecipient(_alice, _bob); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _bob)); + + vm.prank(_alice); + _wrappedMToken.claimFor(_alice); + } + + function test_claimFor_nonEarner() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.prank(_alice); + assertEq(_wrappedMToken.claimFor(_alice), 0); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + } + + function test_claimFor_earner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + assertEq(_wrappedMToken.claimFor(_alice), 100); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_100); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + } + + function test_claimFor_earner_withOverrideRecipient() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_bob))) + ); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _bob, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 100); + + assertEq(_wrappedMToken.claimFor(_alice), 100); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + assertEq(_wrappedMToken.balanceOf(_bob), 100); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 100); + assertEq(_wrappedMToken.totalEarningPrincipal(), 910); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); // round up total earning principal + assertEq(_wrappedMToken.totalAccruedYield(), 1); + } + + function testFuzz_claimFor( + bool earningEnabled_, + bool accountEarning_, + uint240 balanceWithYield_, + uint240 balance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, + bool claimOverride_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); + + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); + + if (claimOverride_) { + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_charlie))) + ); + } + + uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); + + if (accruedYield_ != 0) { + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, claimOverride_ ? _charlie : _alice, accruedYield_); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, accruedYield_); + } + + if (claimOverride_ && (accruedYield_ != 0)) { + vm.expectEmit(); + emit IERC20.Transfer(_alice, _charlie, accruedYield_); + } + + assertEq(_wrappedMToken.claimFor(_alice), accruedYield_); + + assertEq( + _wrappedMToken.totalSupply(), + _wrappedMToken.balanceOf(_alice) + _wrappedMToken.balanceOf(_bob) + _wrappedMToken.balanceOf(_charlie) + ); + } + + /* ============ claimExcess ============ */ + function test_claimExcess_enforcedPause() external { + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + _wrappedMToken.claimExcess(); + } + + function testFuzz_claimExcess( + bool earningEnabled_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, + uint240 totalNonEarningSupply_, + uint240 totalProjectedEarningSupply_, + uint112 mPrincipalBalance_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + uint240 maxAmount_ = _getMaxAmount(_wrappedMToken.currentIndex()); + + totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, maxAmount_)); + + totalProjectedEarningSupply_ = uint240( + bound(totalProjectedEarningSupply_, 0, maxAmount_ - totalNonEarningSupply_) + ); + + uint112 totalEarningPrincipal_ = IndexingMath.getPrincipalAmountRoundedUp( + totalProjectedEarningSupply_, + _wrappedMToken.currentIndex() + ); + + mPrincipalBalance_ = uint112(bound(mPrincipalBalance_, 0, type(uint112).max)); + + _mToken.setPrincipalBalanceOf(address(_wrappedMToken), mPrincipalBalance_); + + uint240 mBalance_ = IndexingMath.getPresentAmountRoundedDown(mPrincipalBalance_, currentMIndex_); + + _mToken.setBalanceOf(address(_wrappedMToken), mBalance_); + + _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); + _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); + + uint240 earmarked_ = totalNonEarningSupply_ + _wrappedMToken.projectedEarningSupply(); + int240 excess_ = int240(mBalance_) - int240(earmarked_); + + if (excess_ > 0) { + vm.expectEmit(false, false, false, false); + emit IWrappedMToken.ExcessClaimed(uint240(excess_)); + } + + uint240 claimed_ = _wrappedMToken.claimExcess(); + + if (excess_ <= 0) { + assertEq(claimed_, 0); + } else { + assertLe(claimed_, uint240(excess_)); + } + + assertEq(_wrappedMToken.balanceOf(_excessDestination), claimed_); + } + + /* ============ transfer ============ */ + function test_transfer_enforcedPause() external { + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 100); + } + + function test_transfer_invalidRecipient() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); + + vm.prank(_alice); + _wrappedMToken.transfer(address(0), 1_000); + } + + function test_transfer_frozenSender() external { + uint256 amount = 1_000; + _wrappedMToken.setAccountOf(_alice, amount); + + // Alice allows Charlie to transfer tokens on her behalf + vm.prank(_alice); + _wrappedMToken.approve(_charlie, amount); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_charlie); + + // Reverts cause Charlie is frozen and cannot transfer tokens on Alice's behalf + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _charlie)); + + vm.prank(_charlie); + _wrappedMToken.transferFrom(_alice, _bob, amount); + } + + function test_transfer_frozenAccount() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 500); + } + + function test_transfer_frozenRecipient() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _bob)); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 500); + } + + function test_transfer_insufficientBalance_toSelf() external { + _wrappedMToken.setAccountOf(_alice, 999); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); + vm.prank(_alice); + _wrappedMToken.transfer(_alice, 1_000); + } + + function test_transfer_insufficientBalance_fromNonEarner_toNonEarner() external { + _wrappedMToken.setAccountOf(_alice, 999); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 1_000); + } + + function test_transfer_fromNonEarner_toNonEarner() external { + _wrappedMToken.setTotalNonEarningSupply(1_500); + + _wrappedMToken.setAccountOf(_alice, 1_000); + _wrappedMToken.setAccountOf(_bob, 500); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 500); + + assertEq(_wrappedMToken.balanceOf(_alice), 500); + + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_500); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + } + + function testFuzz_transfer_fromNonEarner_toNonEarner( + uint256 supply_, + uint256 aliceBalance_, + uint256 transferAmount_ + ) external { + supply_ = bound(supply_, 1, type(uint240).max); + aliceBalance_ = bound(aliceBalance_, 1, supply_); + transferAmount_ = bound(transferAmount_, 1, aliceBalance_); + uint256 bobBalance = supply_ - aliceBalance_; + + _wrappedMToken.setTotalNonEarningSupply(supply_); + + _wrappedMToken.setAccountOf(_alice, aliceBalance_); + _wrappedMToken.setAccountOf(_bob, bobBalance); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, transferAmount_); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, transferAmount_); + + assertEq(_wrappedMToken.balanceOf(_alice), aliceBalance_ - transferAmount_); + assertEq(_wrappedMToken.balanceOf(_bob), bobBalance + transferAmount_); + + assertEq(_wrappedMToken.totalNonEarningSupply(), supply_); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + } + + function test_transfer_fromEarner_toNonEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setTotalNonEarningSupply(500); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_bob, 500); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 500); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 545); + assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); + + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalEarningPrincipal(), 546); + assertEq(_wrappedMToken.totalEarningSupply(), 500); + assertEq(_wrappedMToken.totalAccruedYield(), 101); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 1); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 1); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 544); + assertEq(_wrappedMToken.balanceOf(_alice), 499); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); + + assertEq(_wrappedMToken.balanceOf(_bob), 1_001); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_001); + assertEq(_wrappedMToken.totalEarningPrincipal(), 546); + assertEq(_wrappedMToken.totalEarningSupply(), 499); + assertEq(_wrappedMToken.totalAccruedYield(), 102); + } + + function test_transfer_fromNonEarner_toEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(500); + _wrappedMToken.setTotalEarningSupply(500); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000); + _wrappedMToken.setAccountOf(_bob, 500, 500, false); // 550 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 500); + + assertEq(_wrappedMToken.balanceOf(_alice), 500); + + assertEq(_wrappedMToken.earningPrincipalOf(_bob), 954); + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 49); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 500); + assertEq(_wrappedMToken.totalEarningPrincipal(), 955); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 51); + } + + function test_transfer_fromEarner_toEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_500); + _wrappedMToken.setTotalEarningSupply(1_500); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_bob, 500, 500, false); // 550 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, 500); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 545); + assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); + + assertEq(_wrappedMToken.earningPrincipalOf(_bob), 955); + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_500); + assertEq(_wrappedMToken.totalEarningSupply(), 1_500); + assertEq(_wrappedMToken.totalAccruedYield(), 150); + } + + function test_transfer_nonEarnerToSelf() external { + _wrappedMToken.setTotalNonEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _alice, 500); + + vm.prank(_alice); + _wrappedMToken.transfer(_alice, 500); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); + } + + function test_transfer_earnerToSelf() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _alice, 500); + + vm.prank(_alice); + _wrappedMToken.transfer(_alice, 500); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 100); + } + + function testFuzz_transfer( + bool earningEnabled_, + bool aliceEarning_, + bool bobEarning_, + uint240 aliceBalanceWithYield_, + uint240 aliceBalance_, + uint240 bobBalanceWithYield_, + uint240 bobBalance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, + uint240 amount_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + (aliceBalanceWithYield_, aliceBalance_) = _getFuzzedBalances( + aliceBalanceWithYield_, + aliceBalance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); + + _setupAccount(_alice, aliceEarning_, aliceBalanceWithYield_, aliceBalance_); + + (bobBalanceWithYield_, bobBalance_) = _getFuzzedBalances( + bobBalanceWithYield_, + bobBalance_, + _getMaxAmount(_wrappedMToken.currentIndex()) - aliceBalanceWithYield_ + ); + + _setupAccount(_bob, bobEarning_, bobBalanceWithYield_, bobBalance_); + + amount_ = uint240(bound(amount_, 0, (11 * aliceBalance_) / 10)); + + if (amount_ > aliceBalance_) { + vm.expectRevert( + abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, aliceBalance_, amount_) + ); + } else { + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, amount_); + } + + vm.prank(_alice); + _wrappedMToken.transfer(_bob, amount_); + + if (amount_ > aliceBalance_) return; + + assertEq(_wrappedMToken.balanceOf(_alice), aliceBalance_ - amount_); + assertEq(_wrappedMToken.balanceOf(_bob), bobBalance_ + amount_); + + if (aliceEarning_ && bobEarning_) { + assertEq(_wrappedMToken.totalEarningSupply(), aliceBalance_ + bobBalance_); + } else if (aliceEarning_) { + assertEq(_wrappedMToken.totalEarningSupply(), aliceBalance_ - amount_); + assertEq(_wrappedMToken.totalNonEarningSupply(), bobBalance_ + amount_); + } else if (bobEarning_) { + assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ - amount_); + assertEq(_wrappedMToken.totalEarningSupply(), bobBalance_ + amount_); + } else { + assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ + bobBalance_); + } + } + + /* ============ startEarningFor ============ */ + function test_startEarningFor_enforcedPause() external { + _wrappedMToken.setEnableMIndex(1_100000000000); + + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + _wrappedMToken.startEarningFor(_alice); + } + + function test_startEarningFor_notApprovedEarner() external { + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, _alice)); + _wrappedMToken.startEarningFor(_alice); + } + + function test_startEarningFor_earningIsDisabled() external { + vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); + _wrappedMToken.startEarningFor(_alice); + } + + function test_startEarningFor_frozenAccount() external { + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + _wrappedMToken.startEarningFor(_alice); + } + + function test_startEarning_overflow() external { + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + uint240 aliceBalance_ = uint240(type(uint112).max) + 20; // TODO: _getMaxAmount(1_100000000000) + 2; ? + + _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); + + _wrappedMToken.setAccountOf(_alice, aliceBalance_); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + vm.expectRevert(UIntMath.InvalidUInt112.selector); + _wrappedMToken.startEarningFor(_alice); + } + + function test_startEarningFor() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + vm.expectEmit(); + emit IWrappedMToken.StartedEarning(_alice); + + _wrappedMToken.startEarningFor(_alice); + + assertEq(_wrappedMToken.isEarning(_alice), true); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909); + assertEq(_wrappedMToken.balanceOf(_alice), 1000); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 910); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + } + + function testFuzz_startEarningFor( + uint240 balance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(true, currentMIndex_, enableMIndex_, disableIndex_); + + uint128 currentIndex_ = _wrappedMToken.currentIndex(); + + balance_ = uint240(bound(balance_, 0, _getMaxAmount(currentIndex_))); + + _setupAccount(_alice, false, 0, balance_); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + vm.expectEmit(); + emit IWrappedMToken.StartedEarning(_alice); + + _wrappedMToken.startEarningFor(_alice); + + uint112 principalDown_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); + uint112 principalUp_ = IndexingMath.getPrincipalAmountRoundedUp(balance_, currentIndex_); + + assertEq(_wrappedMToken.isEarning(_alice), true); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), principalDown_); + assertEq(_wrappedMToken.balanceOf(_alice), balance_); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), balance_); + assertEq(_wrappedMToken.totalEarningPrincipal(), principalUp_); + } + + /* ============ startEarningFor batch ============ */ + function test_startEarningFor_batch_enforcedPause() external { + _wrappedMToken.setEnableMIndex(1_100000000000); + + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + _wrappedMToken.startEarningFor(accounts_); + } + + function test_startEarningFor_batch_earningIsDisabled() external { + vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); + _wrappedMToken.startEarningFor(new address[](2)); + } + + function test_startEarningFor_batch_notApprovedEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, _bob)); + _wrappedMToken.startEarningFor(accounts_); + } + + function test_startEarningFor_batch_frozenAccount() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _bob)); + _wrappedMToken.startEarningFor(accounts_); + } + + function test_startEarningFor_batch() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + vm.expectEmit(); + emit IWrappedMToken.StartedEarning(_alice); + + vm.expectEmit(); + emit IWrappedMToken.StartedEarning(_bob); + + _wrappedMToken.startEarningFor(accounts_); + } + + /* ============ stopEarningFor ============ */ + function test_stopEarningFor_isApprovedEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.IsApprovedEarner.selector, _alice)); + _wrappedMToken.stopEarningFor(_alice); + } + + function test_stopEarningFor_enforcedPause() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); + + vm.prank(_pauser); + _wrappedMToken.pause(); + + // stopEarningFor succeeds while paused: skipTransfer=true, yield stays on account. + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + _wrappedMToken.stopEarningFor(_alice); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.isEarning(_alice), false); + } + + function test_stopEarningFor_enforcedPause_withClaimRecipient() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, true); + _wrappedMToken.setInternalClaimRecipient(_alice, _bob); + + assertEq(_wrappedMToken.claimRecipientFor(_alice), _bob); + + vm.prank(_pauser); + _wrappedMToken.pause(); + + // skipTransfer=true: yield stays on `_alice`, so `Claimed` must report `_alice` not `_bob`. + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + _wrappedMToken.stopEarningFor(_alice); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.balanceOf(_bob), 0); + assertEq(_wrappedMToken.isEarning(_alice), false); + } + + function test_stopEarningFor_frozenAccount() external { + _wrappedMToken.setIsEarningOf(_alice, true); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + + _wrappedMToken.stopEarningFor(_alice); + } + + function test_stopEarningFor_frozenClaimRecipient() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, true); // 1_100 balance with yield. + _wrappedMToken.setInternalClaimRecipient(_alice, _bob); + + assertEq(_wrappedMToken.claimRecipientFor(_alice), _bob); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + // Not paused, but the claim recipient _bob is frozen: skipTransfer falls back to true so an + // earner cannot block their own deauthorization by pointing yield at a frozen recipient. + // The yield stays on _alice, so `Claimed` reports _alice not _bob. + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + _wrappedMToken.stopEarningFor(_alice); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.balanceOf(_bob), 0); + assertEq(_wrappedMToken.isEarning(_alice), false); + } + + function test_stopEarningFor() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + _wrappedMToken.stopEarningFor(_alice); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.isEarning(_alice), false); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_100); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + } + + function testFuzz_stopEarningFor( + bool earningEnabled_, + uint240 balanceWithYield_, + uint240 balance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); + + _setupAccount(_alice, true, balanceWithYield_, balance_); + + uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); + + if (accruedYield_ != 0) { + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, accruedYield_); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, accruedYield_); + } + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + _wrappedMToken.stopEarningFor(_alice); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.isEarning(_alice), false); + + assertEq(_wrappedMToken.totalNonEarningSupply(), balance_ + accruedYield_); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + } + + /* ============ setClaimRecipient ============ */ + function test_setClaimRecipient() external { + (, , , bool hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); + + assertFalse(hasClaimRecipient_); + assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), address(0)); + + vm.prank(_alice); + _wrappedMToken.setClaimRecipient(_alice); + + (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); + + assertTrue(hasClaimRecipient_); + assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), _alice); + + vm.prank(_alice); + _wrappedMToken.setClaimRecipient(_bob); + + (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); + + assertTrue(hasClaimRecipient_); + assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), _bob); + + vm.prank(_alice); + _wrappedMToken.setClaimRecipient(address(0)); + + (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); + + assertFalse(hasClaimRecipient_); + assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), address(0)); + } + + /* ============ stopEarningFor batch ============ */ + function test_stopEarningFor_batch_isApprovedEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.IsApprovedEarner.selector, _bob)); + _wrappedMToken.stopEarningFor(accounts_); + } + + function test_stopEarningFor_batch() external { + _wrappedMToken.setAccountOf(_alice, 0, 0, false); + _wrappedMToken.setAccountOf(_bob, 0, 0, false); + + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_bob); + + _wrappedMToken.stopEarningFor(accounts_); + } + + function test_stopEarningFor_batch_frozenClaimRecipient() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(2_000); + _wrappedMToken.setTotalEarningSupply(2_000); + + // _alice routes yield to a frozen recipient, _bob to an unfrozen one. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, true); // 1_100 balance with yield. + _wrappedMToken.setInternalClaimRecipient(_alice, _charlie); + + _wrappedMToken.setAccountOf(_bob, 1_000, 1_000, true); // 1_100 balance with yield. + _wrappedMToken.setInternalClaimRecipient(_bob, _david); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_charlie); + + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + // The frozen recipient decision is per account: _alice falls back to skipTransfer so her yield + // stays on her (Claimed reports _alice), while _bob's yield still routes to the unfrozen _david. + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_bob, _david, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_bob); + + _wrappedMToken.stopEarningFor(accounts_); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); // yield retained, frozen recipient got nothing + assertEq(_wrappedMToken.balanceOf(_charlie), 0); + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); // yield routed out to _david + assertEq(_wrappedMToken.balanceOf(_david), 100); + assertEq(_wrappedMToken.isEarning(_alice), false); + assertEq(_wrappedMToken.isEarning(_bob), false); + } + + /* ============ freeze / _beforeFreeze ============ */ + function test_freeze_claimsAccruedYield() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + vm.expectEmit(); + emit IFreezable.Frozen(_alice, block.timestamp); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.isEarning(_alice), false); + assertTrue(_wrappedMToken.isFrozen(_alice)); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_100); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + } + + function test_freeze_skipsClaimRecipientRouting() external { + // `_beforeFreeze` claims with skipTransfer=true, so the freeze keeps the yield on the + // account being frozen instead of routing it to the claim recipient. This both keeps + // the full balance seizable via `forceTransfer` and ensures freezing is never blocked + // by a frozen claim recipient (which would otherwise revert the routing transfer). + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, true); + _wrappedMToken.setInternalClaimRecipient(_alice, _bob); // has explicit claim recipient + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + vm.expectEmit(); + emit IFreezable.Frozen(_alice, block.timestamp); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); // yield retained on frozen account + assertEq(_wrappedMToken.balanceOf(_bob), 0); // not routed to claim recipient + assertEq(_wrappedMToken.isEarning(_alice), false); + assertTrue(_wrappedMToken.isFrozen(_alice)); + } + + function test_freeze_whilePaused() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectEmit(); + emit IFreezable.Frozen(_alice, block.timestamp); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + assertTrue(_wrappedMToken.isFrozen(_alice)); + } + + function test_freeze_approvedEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); + + // _alice is an approved earner — freeze still works. + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + + vm.expectEmit(); + emit IFreezable.Frozen(_alice, block.timestamp); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.isEarning(_alice), false); + assertTrue(_wrappedMToken.isFrozen(_alice)); + } + + /* ============ forceTransfer ============ */ + function test_forceTransfer_unauthorized() external { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + _alice, + _FORCED_TRANSFER_MANAGER_ROLE + ) + ); + + vm.prank(_alice); + _wrappedMToken.forceTransfer(_alice, _bob, 1_000); + } + + function test_forceTransfer_notFrozen() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountNotFrozen.selector, _alice)); + + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, _bob, 500); + } + + function test_forceTransfer_frozenEarnerCannotBeReEnabled() external { + // Regression: a frozen account must not be re-enabled as an earner, otherwise + // `forceTransfer` would run `_subtractNonEarningAmount` on an earning account and + // corrupt supply accounting. + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); + + // `_alice` remains an approved earner even after being frozen. + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + // Freezing claimed yield (100) and stopped earning, moving the balance to non-earning. + assertEq(_wrappedMToken.isEarning(_alice), false); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_100); + + // Re-enabling earning on the still-frozen account must revert. + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _alice)); + _wrappedMToken.startEarningFor(_alice); + + // The account stays non-earning, so `forceTransfer` keeps accounting correct. + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, _bob, 500); + + assertEq(_wrappedMToken.balanceOf(_alice), 600); + assertEq(_wrappedMToken.balanceOf(_bob), 500); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_100); + } + + function test_forceTransfer_invalidRecipient() external { + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); + + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, address(0), 500); + } + + function test_forceTransfer_frozenRecipient() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_bob); + + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _bob)); + + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, _bob, 500); + } + + function test_forceTransfer_zeroAmount() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 0); + + vm.expectEmit(); + emit IForcedTransferable.ForcedTransfer(_alice, _bob, _forcedTransferManager, 0); + + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, _bob, 0); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_bob), 0); + } + + function test_forceTransfer_insufficientBalance() external { + _wrappedMToken.setAccountOf(_alice, 1_000); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 1_000, 2_000)); + + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, _bob, 2_000); + } + + function test_forceTransfer_toNonEarner() external { + _wrappedMToken.setTotalNonEarningSupply(1_500); + + _wrappedMToken.setAccountOf(_alice, 1_000); + _wrappedMToken.setAccountOf(_bob, 500); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_500); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + + vm.expectEmit(); + emit IForcedTransferable.ForcedTransfer(_alice, _bob, _forcedTransferManager, 500); + + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, _bob, 500); + + assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + + // Net totalNonEarningSupply unchanged (moved within non-earning). + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_500); + } + + function test_forceTransfer_toEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + + _wrappedMToken.setTotalEarningPrincipal(500); + _wrappedMToken.setTotalEarningSupply(500); + + _wrappedMToken.setAccountOf(_alice, 1_000); + _wrappedMToken.setAccountOf(_bob, 500, 500, false); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + + vm.expectEmit(); + emit IForcedTransferable.ForcedTransfer(_alice, _bob, _forcedTransferManager, 500); + + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, _bob, 500); + + assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 500); // decreased by 500 + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); // increased by 500 + } + + function test_forceTransfer_whilePaused() external { + _wrappedMToken.setTotalNonEarningSupply(1_500); + + _wrappedMToken.setAccountOf(_alice, 1_000); + _wrappedMToken.setAccountOf(_bob, 500); + + vm.prank(_freezeManager); + _wrappedMToken.freeze(_alice); + + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + + vm.expectEmit(); + emit IForcedTransferable.ForcedTransfer(_alice, _bob, _forcedTransferManager, 500); + + vm.prank(_forcedTransferManager); + _wrappedMToken.forceTransfer(_alice, _bob, 500); + + assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + } + + /* ============ enableEarning ============ */ + function test_enableEarning_notApprovedEarner() external { + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, address(_wrappedMToken))); + _wrappedMToken.enableEarning(); + } + + function test_enableEarning() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _mToken.setCurrentIndex(1_210000000000); + + assertEq(_wrappedMToken.enableMIndex(), 0); + assertEq(_wrappedMToken.currentIndex(), 1_000000000000); + + vm.expectEmit(); + emit IWrappedMToken.EarningEnabled(1_210000000000); + + _wrappedMToken.enableEarning(); + + assertEq(_wrappedMToken.enableMIndex(), 1_210000000000); + assertEq(_wrappedMToken.currentIndex(), 1_000000000000); + } + + /* ============ disableEarning ============ */ + function test_disableEarning_earningIsDisabled() external { + vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); + _wrappedMToken.disableEarning(); + } + + function test_disableEarning_approvedEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.IsApprovedEarner.selector, address(_wrappedMToken))); + _wrappedMToken.disableEarning(); + } + + function test_disableEarning() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + assertEq(_wrappedMToken.enableMIndex(), 1_100000000000); + assertEq(_wrappedMToken.disableIndex(), 0); + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); + + vm.expectEmit(); + emit IWrappedMToken.EarningDisabled(1_100000000000); + + _wrappedMToken.disableEarning(); + + assertEq(_wrappedMToken.enableMIndex(), 0); + assertEq(_wrappedMToken.disableIndex(), 1_100000000000); + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); + } + + /* ============ balanceOf ============ */ + function test_balanceOf_nonEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setAccountOf(_alice, 500); + + assertEq(_wrappedMToken.balanceOf(_alice), 500); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + } + + function test_balanceOf_earner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. + + assertEq(_wrappedMToken.balanceOf(_alice), 500); + + _wrappedMToken.setEarningPrincipalOf(_alice, 1_000); // Earning principal has no bearing on balance. + + assertEq(_wrappedMToken.balanceOf(_alice), 500); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + } + + /* ============ balanceWithYieldOf ============ */ + function test_balanceWithYieldOf_nonEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setAccountOf(_alice, 500); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 500); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + } + + function test_balanceWithYieldOf_earner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 550); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_210); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_815); + } + + /* ============ accruedYieldOf ============ */ + function test_accruedYieldOf_nonEarner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setAccountOf(_alice, 500); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + } + + function test_accruedYieldOf_earner() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 50); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 210); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 815); + } + + /* ============ earningPrincipalOf ============ */ + function test_earningPrincipalOf() external { + _wrappedMToken.setAccountOf(_alice, 0, 100, false); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 100); + + _wrappedMToken.setAccountOf(_alice, 0, 200, false); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 200); + } + + /* ============ isEarning ============ */ + function test_isEarning() external { + _wrappedMToken.setAccountOf(_alice, 0); + + assertFalse(_wrappedMToken.isEarning(_alice)); + + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); + + assertTrue(_wrappedMToken.isEarning(_alice)); + } + + /* ============ isEarningEnabled ============ */ + function test_isEarningEnabled() external { + assertFalse(_wrappedMToken.isEarningEnabled()); + + _wrappedMToken.setEnableMIndex(1_100000000000); + + assertTrue(_wrappedMToken.isEarningEnabled()); + + _wrappedMToken.setEnableMIndex(0); + + assertFalse(_wrappedMToken.isEarningEnabled()); + + _wrappedMToken.setEnableMIndex(1_100000000000); + + assertTrue(_wrappedMToken.isEarningEnabled()); + } + + /* ============ claimRecipientFor ============ */ + function test_claimRecipientFor() external view { + assertEq(_wrappedMToken.claimRecipientFor(_alice), _alice); + } + + function test_claimRecipientFor_hasClaimRecipient() external { + _wrappedMToken.setAccountOf(_alice, 0, 0, true); + _wrappedMToken.setInternalClaimRecipient(_alice, _bob); + + assertEq(_wrappedMToken.claimRecipientFor(_alice), _bob); + } + + function test_claimRecipientFor_hasClaimOverrideRecipient() external { + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_charlie))) + ); + + assertEq(_wrappedMToken.claimRecipientFor(_alice), _charlie); + } + + function test_claimRecipientFor_hasClaimRecipientAndOverrideRecipient() external { + _wrappedMToken.setAccountOf(_alice, 0, 0, true); + _wrappedMToken.setInternalClaimRecipient(_alice, _bob); + + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_charlie))) + ); + + assertEq(_wrappedMToken.claimRecipientFor(_alice), _bob); + } + + /* ============ totalSupply ============ */ + function test_totalSupply_onlyTotalNonEarningSupply() external { + _wrappedMToken.setTotalNonEarningSupply(500); + + assertEq(_wrappedMToken.totalSupply(), 500); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + + assertEq(_wrappedMToken.totalSupply(), 1_000); + } + + function test_totalSupply_onlyTotalEarningSupply() external { + _wrappedMToken.setTotalEarningSupply(500); + + assertEq(_wrappedMToken.totalSupply(), 500); + + _wrappedMToken.setTotalEarningSupply(1_000); + + assertEq(_wrappedMToken.totalSupply(), 1_000); + } + + function test_totalSupply() external { + _wrappedMToken.setTotalEarningSupply(400); + + _wrappedMToken.setTotalNonEarningSupply(600); + + assertEq(_wrappedMToken.totalSupply(), 1_000); + + _wrappedMToken.setTotalEarningSupply(700); + + assertEq(_wrappedMToken.totalSupply(), 1_300); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + + assertEq(_wrappedMToken.totalSupply(), 1_700); + } + + /* ============ currentIndex ============ */ + function test_currentIndex() external { + assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); + + _wrappedMToken.setDisableIndex(1_050000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_050000000000); + + _wrappedMToken.setDisableIndex(1_100000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); + + _wrappedMToken.setEnableMIndex(1_100000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_331000000000); + + _wrappedMToken.setEnableMIndex(1_155000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_267619047619); + + _wrappedMToken.setEnableMIndex(1_210000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_210000000000); + + _wrappedMToken.setEnableMIndex(1_270500000000); + + assertEq(_wrappedMToken.currentIndex(), 1_152380952380); + + _wrappedMToken.setEnableMIndex(1_331000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); + + _mToken.setCurrentIndex(1_464100000000); + + assertEq(_wrappedMToken.currentIndex(), 1_210000000000); + } + + /* ============ excess ============ */ + function test_excess() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + assertEq(_wrappedMToken.excess(), 0); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_100); + + assertEq(_wrappedMToken.excess(), 0); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_101); + + assertEq(_wrappedMToken.excess(), 1); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_102); + + assertEq(_wrappedMToken.excess(), 2); + + _mToken.setBalanceOf(address(_wrappedMToken), 3_102); + + assertEq(_wrappedMToken.excess(), 1_002); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.excess(), 892); + } + + function testFuzz_excess( + bool earningEnabled_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, + uint240 totalNonEarningSupply_, + uint240 totalProjectedEarningSupply_, + uint112 mPrincipalBalance_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + uint240 maxAmount_ = _getMaxAmount(_wrappedMToken.currentIndex()); + + totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, maxAmount_)); + + totalProjectedEarningSupply_ = uint240( + bound(totalProjectedEarningSupply_, 0, maxAmount_ - totalNonEarningSupply_) + ); + + uint112 totalEarningPrincipal_ = IndexingMath.getPrincipalAmountRoundedUp( + totalProjectedEarningSupply_, + _wrappedMToken.currentIndex() + ); + + mPrincipalBalance_ = uint112(bound(mPrincipalBalance_, 0, type(uint112).max)); + + _mToken.setPrincipalBalanceOf(address(_wrappedMToken), mPrincipalBalance_); + + uint240 mBalance_ = IndexingMath.getPresentAmountRoundedDown(mPrincipalBalance_, currentMIndex_); + + _mToken.setBalanceOf(address(_wrappedMToken), mBalance_); + + _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); + _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); + + uint240 earmarked_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; + + assertLe(_wrappedMToken.excess(), int248(uint248(mBalance_)) - int248(uint248(earmarked_))); + } + + /* ============ totalAccruedYield ============ */ + function test_totalAccruedYield() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _wrappedMToken.setTotalEarningPrincipal(909); + _wrappedMToken.setTotalEarningSupply(1_000); + + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + _wrappedMToken.setTotalEarningPrincipal(1_000); + + assertEq(_wrappedMToken.totalAccruedYield(), 100); + + _wrappedMToken.setTotalEarningSupply(900); + + assertEq(_wrappedMToken.totalAccruedYield(), 200); + + _mToken.setCurrentIndex(1_331000000000); + + assertEq(_wrappedMToken.totalAccruedYield(), 310); + } + + /* ============ utils ============ */ + function _getPrincipalAmountRoundedDown(uint240 presentAmount_, uint128 index_) internal pure returns (uint112) { + return IndexingMath.divide240By128Down(presentAmount_, index_); + } + + function _getPresentAmountRoundedDown(uint112 principalAmount_, uint128 index_) internal pure returns (uint240) { + return IndexingMath.multiply112By128Down(principalAmount_, index_); + } + + function _getMaxAmount(uint128 index_) internal pure returns (uint240 maxAmount_) { + return (uint240(type(uint112).max) * index_) / _EXP_SCALED_ONE; + } + + function _getFuzzedIndices( + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) internal pure returns (uint128, uint128, uint128) { + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + enableMIndex_ = uint128(bound(enableMIndex_, _EXP_SCALED_ONE, currentMIndex_)); + + disableIndex_ = uint128( + bound(disableIndex_, _EXP_SCALED_ONE, (currentMIndex_ * _EXP_SCALED_ONE) / enableMIndex_) + ); + + return (currentMIndex_, enableMIndex_, disableIndex_); + } + + function _setupIndexes( + bool earningEnabled_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) internal { + _mToken.setCurrentIndex(currentMIndex_); + _wrappedMToken.setDisableIndex(disableIndex_); + + if (earningEnabled_) { + _mToken.setIsEarning(address(_wrappedMToken), true); + _wrappedMToken.setEnableMIndex(enableMIndex_); + } + } + + function _getFuzzedBalances( + uint240 balanceWithYield_, + uint240 balance_, + uint240 maxAmount_ + ) internal view returns (uint240, uint240) { + uint128 currentIndex_ = _wrappedMToken.currentIndex(); + + balanceWithYield_ = uint240(bound(balanceWithYield_, 0, maxAmount_)); + balance_ = uint240(bound(balance_, (balanceWithYield_ * _EXP_SCALED_ONE) / currentIndex_, balanceWithYield_)); + + return (balanceWithYield_, balance_); + } + + function _setupAccount( + address account_, + bool accountEarning_, + uint240 balanceWithYield_, + uint240 balance_ + ) internal { + if (accountEarning_) { + uint112 principal_ = IndexingMath.getPrincipalAmountRoundedDown( + balanceWithYield_, + _wrappedMToken.currentIndex() + ); + + _wrappedMToken.setAccountOf(account_, balance_, principal_, false); + _wrappedMToken.setTotalEarningPrincipal(_wrappedMToken.totalEarningPrincipal() + principal_); + _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + balance_); + } else { + _wrappedMToken.setAccountOf(account_, balance_); + _wrappedMToken.setTotalNonEarningSupply(_wrappedMToken.totalNonEarningSupply() + balance_); + } + } +} diff --git a/test/utils/BaseUnitTest.sol b/test/utils/BaseUnitTest.sol new file mode 100644 index 0000000..93992a7 --- /dev/null +++ b/test/utils/BaseUnitTest.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { Test } from "../../lib/forge-std/src/Test.sol"; + +import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; + +import { WrappedMTokenHarness } from "../harness/WrappedMTokenHarness.sol"; + +import { MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; + +contract BaseUnitTest is Test { + uint56 internal constant _EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; + + uint56 internal constant _ONE_HUNDRED_PERCENT = 10_000; + + bytes32 internal constant _CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX = "wm_claim_override_recipient"; + bytes32 internal constant _FREEZE_MANAGER_ROLE = keccak256("FREEZE_MANAGER_ROLE"); + bytes32 internal constant _FORCED_TRANSFER_MANAGER_ROLE = keccak256("FORCED_TRANSFER_MANAGER_ROLE"); + bytes32 internal constant _PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 internal constant _EXCESS_MANAGER_ROLE = keccak256("EXCESS_MANAGER_ROLE"); + + bytes32 internal constant _EARNERS_LIST_NAME = "earners"; + + address internal _alice = makeAddr("alice"); + address internal _bob = makeAddr("bob"); + address internal _charlie = makeAddr("charlie"); + address internal _david = makeAddr("david"); + + address internal _admin = makeAddr("admin"); + address internal _excessManager = makeAddr("excessManager"); + address internal _excessDestination = makeAddr("excessDestination"); + address internal _freezeManager = makeAddr("freezeManager"); + address internal _forcedTransferManager = makeAddr("forcedTransferManager"); + address internal _migrationAdmin = makeAddr("migrationAdmin"); + address internal _pauser = makeAddr("pauser"); + + address[] internal _accounts = [_alice, _bob, _charlie, _david]; + + MockM internal _mToken; + MockRegistrar internal _registrar; + MockSwapFacility internal _swapFacility; + WrappedMTokenHarness internal _wrappedMToken; +} diff --git a/test/utils/Invariants.sol b/test/utils/Invariants.sol index 086cc1f..badd494 100644 --- a/test/utils/Invariants.sol +++ b/test/utils/Invariants.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; +import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; // import { console2 } from "../../lib/forge-std/src/Test.sol"; import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; -import { IndexingMath } from "../../src/libs/IndexingMath.sol"; - import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; library Invariants { @@ -83,34 +82,30 @@ library Invariants { // ); return - IERC20(mToken_).balanceOf(wrappedMToken_) >= - IWrappedMToken(wrappedMToken_).totalSupply() + - IWrappedMToken(wrappedMToken_).totalAccruedYield() + + int256(IERC20(mToken_).balanceOf(wrappedMToken_)) >= + int256(IWrappedMToken(wrappedMToken_).totalSupply() + IWrappedMToken(wrappedMToken_).totalAccruedYield()) + IWrappedMToken(wrappedMToken_).excess(); } // Invariant 4: Sum of all earning accounts' principals is less than or equal to principal of total earning supply. function checkInvariant4(address wrappedMToken_, address[] memory accounts_) internal view returns (bool success_) { - uint256 principalOfTotalEarningSupply_; + uint256 totalEarningPrincipal_; for (uint256 i_; i_ < accounts_.length; ++i_) { address account_ = accounts_[i_]; if (!IWrappedMToken(wrappedMToken_).isEarning(account_)) continue; - principalOfTotalEarningSupply_ += IndexingMath.getPrincipalAmountRoundedDown( - uint240(IWrappedMToken(wrappedMToken_).balanceOf(account_)), - IWrappedMToken(wrappedMToken_).lastIndexOf(account_) - ); + totalEarningPrincipal_ += IWrappedMToken(wrappedMToken_).earningPrincipalOf(account_); } - // console2.log("Invariant 2: principalOfTotalEarningSupply_ = %d", principalOfTotalEarningSupply_); + // console2.log("Invariant 2: totalEarningPrincipal_ = %d", totalEarningPrincipal_); // console2.log( - // "Invariant 2: principalOfTotalEarningSupply() = %d", - // IWrappedMToken(wrappedMToken_).principalOfTotalEarningSupply() + // "Invariant 2: totalEarningPrincipal() = %d", + // IWrappedMToken(wrappedMToken_).totalEarningPrincipal() // ); - return IWrappedMToken(wrappedMToken_).principalOfTotalEarningSupply() >= principalOfTotalEarningSupply_; + return IWrappedMToken(wrappedMToken_).totalEarningPrincipal() >= totalEarningPrincipal_; } } diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index 56968c1..db539b7 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -1,14 +1,33 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; -contract MockM { - address public ttgRegistrar; +import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; +contract MockM { uint128 public currentIndex; mapping(address account => uint256 balance) public balanceOf; mapping(address account => bool isEarning) public isEarning; + mapping(address account => uint240 principal) public principalBalanceOf; + + function permit( + address owner_, + address spender_, + uint256 value_, + uint256 deadline_, + uint8 v_, + bytes32 r_, + bytes32 s_ + ) external {} + + function permit( + address owner_, + address spender_, + uint256 value_, + uint256 deadline_, + bytes memory signature_ + ) external {} function transfer(address recipient_, uint256 amount_) external returns (bool success_) { balanceOf[msg.sender] -= amount_; @@ -28,12 +47,16 @@ contract MockM { balanceOf[account_] = balance_; } + function setPrincipalBalanceOf(address account_, uint240 principal_) external { + principalBalanceOf[account_] = principal_; + } + function setCurrentIndex(uint128 currentIndex_) external { currentIndex = currentIndex_; } - function setTtgRegistrar(address ttgRegistrar_) external { - ttgRegistrar = ttgRegistrar_; + function setIsEarning(address account_, bool isEarning_) external { + isEarning[account_] = isEarning_; } function startEarning() external { @@ -43,11 +66,13 @@ contract MockM { function stopEarning() external { isEarning[msg.sender] = false; } + + function approve(address /* spender_ */, uint256 /* amount_ */) external pure returns (bool success_) { + return true; + } } contract MockRegistrar { - address public vault; - mapping(bytes32 key => bytes32 value) public get; mapping(bytes32 list => mapping(address account => bool contains)) public listContains; @@ -59,8 +84,33 @@ contract MockRegistrar { function setListContains(bytes32 list_, address account_, bool contains_) external { listContains[list_][account_] = contains_; } +} + +interface IMExtension { + function wrap(address recipient, uint256 amount) external; + function unwrap(address recipient, uint256 amount) external; +} + +contract MockSwapFacility { + address public immutable mToken; + + constructor(address mToken_) { + mToken = mToken_; + } + + function swapInM(address extensionOut, uint256 amount, address recipient) external { + IERC20(mToken).transferFrom(msg.sender, address(this), amount); + IERC20(mToken).approve(extensionOut, amount); + IMExtension(extensionOut).wrap(recipient, amount); + } + + function swapOutM(address extensionIn, uint256 amount, address recipient) external { + IERC20(extensionIn).transferFrom(msg.sender, address(this), amount); + + uint256 balanceBefore = IERC20(mToken).balanceOf(address(this)); + IMExtension(extensionIn).unwrap(address(this), amount); - function setVault(address vault_) external { - vault = vault_; + amount = IERC20(mToken).balanceOf(address(this)) - balanceBefore; + IERC20(mToken).transfer(recipient, amount); } } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol deleted file mode 100644 index 9bc9868..0000000 --- a/test/utils/WrappedMTokenHarness.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity 0.8.23; - -import { WrappedMToken } from "../../src/WrappedMToken.sol"; - -contract WrappedMTokenHarness is WrappedMToken { - constructor(address mToken_, address migrationAdmin_) WrappedMToken(mToken_, migrationAdmin_) {} - - function setIsEarningOf(address account_, bool isEarning_) external { - _accounts[account_].isEarning = isEarning_; - } - - function setLastIndexOf(address account_, uint256 index_) external { - _accounts[account_].lastIndex = uint128(index_); - } - - function setAccountOf(address account_, uint256 balance_, uint256 index_) external { - _accounts[account_] = Account(true, uint240(balance_), uint128(index_)); - } - - function setAccountOf(address account_, uint256 balance_) external { - _accounts[account_] = Account(false, uint240(balance_), 0); - } - - function setTotalNonEarningSupply(uint256 totalNonEarningSupply_) external { - totalNonEarningSupply = uint240(totalNonEarningSupply_); - } - - function setTotalEarningSupply(uint256 totalEarningSupply_) external { - totalEarningSupply = uint240(totalEarningSupply_); - } - - function setPrincipalOfTotalEarningSupply(uint256 principalOfTotalEarningSupply_) external { - principalOfTotalEarningSupply = uint112(principalOfTotalEarningSupply_); - } - - function getAccountOf(address account_) external view returns (bool isEarning_, uint240 balance_, uint128 index_) { - Account storage account = _accounts[account_]; - return (account.isEarning, account.balance, account.lastIndex); - } -} diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 375010f..0000000 --- a/yarn.lock +++ /dev/null @@ -1,1113 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" - integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== - dependencies: - "@babel/highlight" "^7.24.7" - picocolors "^1.0.0" - -"@babel/helper-validator-identifier@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" - integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== - -"@babel/highlight@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" - integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== - dependencies: - "@babel/helper-validator-identifier" "^7.24.7" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@pnpm/config.env-replace@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" - integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== - -"@pnpm/network.ca-file@^1.0.1": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" - integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== - dependencies: - graceful-fs "4.2.10" - -"@pnpm/npm-conf@^2.1.0": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz#0058baf1c26cbb63a828f0193795401684ac86f0" - integrity sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA== - dependencies: - "@pnpm/config.env-replace" "^1.1.0" - "@pnpm/network.ca-file" "^1.0.1" - config-chain "^1.1.11" - -"@prettier/sync@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@prettier/sync/-/sync-0.3.0.tgz#91f2cfc23490a21586d1cf89c6f72157c000ca1e" - integrity sha512-3dcmCyAxIcxy036h1I7MQU/uEEBq8oLwf1CE3xeze+MPlgkdlb/+w6rGR/1dhp6Hqi17fRS6nvwnOzkESxEkOw== - -"@sindresorhus/is@^5.2.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" - integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== - -"@solidity-parser/parser@^0.17.0": - version "0.17.0" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.17.0.tgz#52a2fcc97ff609f72011014e4c5b485ec52243ef" - integrity sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw== - -"@solidity-parser/parser@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.18.0.tgz#8e77a02a09ecce957255a2f48c9a7178ec191908" - integrity sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA== - -"@szmarczak/http-timer@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" - integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== - dependencies: - defer-to-connect "^2.0.1" - -"@types/http-cache-semantics@^4.0.2": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" - integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== - -ajv@^6.12.6: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.1: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - -ansi-escapes@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" - integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.0.0, ansi-styles@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -antlr4@^4.13.1-patch-1: - version "4.13.1-patch-1" - resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.1-patch-1.tgz#946176f863f890964a050c4f18c47fd6f7e57602" - integrity sha512-OjFLWWLzDMV9rdFhpvroCWR4ooktNg9/nvVYSA5z28wuVpU36QUNuioR1XLnQtcjVlf8npjyz593PxnU/f/Cow== - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -ast-parents@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/ast-parents/-/ast-parents-0.0.1.tgz#508fd0f05d0c48775d9eccda2e174423261e8dd3" - integrity sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA== - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -cacheable-lookup@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" - integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== - -cacheable-request@^10.2.8: - version "10.2.14" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" - integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== - 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" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -cli-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" - integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== - dependencies: - restore-cursor "^4.0.0" - -cli-truncate@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" - integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== - dependencies: - slice-ansi "^5.0.0" - string-width "^7.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@^2.0.20: - version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -commander@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" - integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== - -commander@~12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - -config-chain@^1.1.11: - version "1.1.13" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -cosmiconfig@^8.0.0: - version "8.3.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== - dependencies: - import-fresh "^3.3.0" - js-yaml "^4.1.0" - parse-json "^5.2.0" - path-type "^4.0.0" - -cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -debug@~4.3.4: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== - dependencies: - ms "2.1.2" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -defer-to-connect@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - -emoji-regex@^10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" - integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== - -execa@~8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" - integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^8.0.1" - human-signals "^5.0.0" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^4.1.0" - strip-final-newline "^3.0.0" - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-diff@^1.1.2, fast-diff@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" - integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-uri@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" - integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -form-data-encoder@^2.1.2: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" - integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -get-east-asian-width@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" - integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== - -get-stream@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -get-stream@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" - integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== - -glob@^8.0.3: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -got@^12.1.0: - version "12.6.1" - resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" - integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== - 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" - -graceful-fs@4.2.10: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -http-cache-semantics@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http2-wrapper@^2.1.10: - version "2.2.1" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" - integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.2.0" - -human-signals@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" - integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== - -ignore@^5.2.4: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== - -import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@^1.3.4, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-fullwidth-code-point@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" - integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== - -is-fullwidth-code-point@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" - integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== - dependencies: - get-east-asian-width "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -latest-version@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" - integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== - dependencies: - package-json "^8.1.0" - -lilconfig@~3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" - integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -lint-staged@^15.2.2: - version "15.2.7" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.7.tgz#97867e29ed632820c0fb90be06cd9ed384025649" - integrity sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw== - dependencies: - chalk "~5.3.0" - commander "~12.1.0" - debug "~4.3.4" - execa "~8.0.1" - lilconfig "~3.1.1" - listr2 "~8.2.1" - micromatch "~4.0.7" - pidtree "~0.6.0" - string-argv "~0.3.2" - yaml "~2.4.2" - -listr2@~8.2.1: - version "8.2.3" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.3.tgz#c494bb89b34329cf900e4e0ae8aeef9081d7d7a5" - integrity sha512-Lllokma2mtoniUOS94CcOErHWAug5iu7HOmDrvWgpw8jyQH2fomgB+7lZS4HWZxytUuQwkGOwe49FvwVaA85Xw== - dependencies: - cli-truncate "^4.0.0" - colorette "^2.0.20" - eventemitter3 "^5.0.1" - log-update "^6.0.0" - rfdc "^1.4.1" - wrap-ansi "^9.0.0" - -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-update@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" - integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== - dependencies: - ansi-escapes "^6.2.0" - cli-cursor "^4.0.0" - slice-ansi "^7.0.0" - strip-ansi "^7.1.0" - wrap-ansi "^9.0.0" - -lowercase-keys@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" - integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -micromatch@~4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -mimic-response@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" - integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.0: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -normalize-url@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" - integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== - -npm-run-path@^5.1.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" - integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== - dependencies: - path-key "^4.0.0" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - -p-cancelable@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" - integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== - -package-json@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" - integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== - dependencies: - got "^12.1.0" - registry-auth-token "^5.0.1" - registry-url "^6.0.0" - semver "^7.3.7" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - 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" - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-key@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" - integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picocolors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pidtree@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" - integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== - -pluralize@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" - integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== - -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier-plugin-solidity@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz#59944d3155b249f7f234dee29f433524b9a4abcf" - integrity sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA== - dependencies: - "@solidity-parser/parser" "^0.17.0" - semver "^7.5.4" - solidity-comments-extractor "^0.0.8" - -prettier@^2.8.3: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== - -prettier@^3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - -rc@1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -registry-auth-token@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" - integrity sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ== - dependencies: - "@pnpm/npm-conf" "^2.1.0" - -registry-url@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-6.0.1.tgz#056d9343680f2f64400032b1e199faa692286c58" - integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== - dependencies: - rc "1.2.8" - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -resolve-alpn@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -responselike@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" - integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== - dependencies: - lowercase-keys "^3.0.0" - -restore-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" - integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -rfdc@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" - integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== - -semver@^7.3.7, semver@^7.5.2, semver@^7.5.4: - version "7.6.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" - integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -signal-exit@^3.0.2: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slice-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" - integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== - dependencies: - ansi-styles "^6.0.0" - is-fullwidth-code-point "^4.0.0" - -slice-ansi@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" - integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== - dependencies: - ansi-styles "^6.2.1" - is-fullwidth-code-point "^5.0.0" - -solhint-plugin-prettier@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/solhint-plugin-prettier/-/solhint-plugin-prettier-0.1.0.tgz#2f46999e26d6c6bc80281c22a7a21e381175bef7" - integrity sha512-SDOTSM6tZxZ6hamrzl3GUgzF77FM6jZplgL2plFBclj/OjKP8Z3eIPojKU73gRr0MvOS8ACZILn8a5g0VTz/Gw== - dependencies: - "@prettier/sync" "^0.3.0" - prettier-linter-helpers "^1.0.0" - -solhint@^4.5.2: - version "4.5.4" - resolved "https://registry.yarnpkg.com/solhint/-/solhint-4.5.4.tgz#171cf33f46c36b8499efe60c0e425f6883a54e50" - integrity sha512-Cu1XiJXub2q1eCr9kkJ9VPv1sGcmj3V7Zb76B0CoezDOB9bu3DxKIFFH7ggCl9fWpEPD6xBmRLfZrYijkVmujQ== - dependencies: - "@solidity-parser/parser" "^0.18.0" - ajv "^6.12.6" - antlr4 "^4.13.1-patch-1" - ast-parents "^0.0.1" - 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" - strip-ansi "^6.0.1" - table "^6.8.1" - text-table "^0.2.0" - optionalDependencies: - prettier "^2.8.3" - -solidity-comments-extractor@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz#f6e148ab0c49f30c1abcbecb8b8df01ed8e879f8" - integrity sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g== - -string-argv@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" - integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== - -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" - integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== - dependencies: - emoji-regex "^10.3.0" - get-east-asian-width "^1.0.0" - strip-ansi "^7.1.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -table@^6.8.1: - version "6.8.2" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58" - integrity sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA== - 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" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wrap-ansi@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" - integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== - dependencies: - ansi-styles "^6.2.1" - string-width "^7.0.0" - strip-ansi "^7.1.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -yaml@~2.4.2: - version "2.4.5" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" - integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==