From 2db6f811567150b7aaa5bd57680099b544a8a572 Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:00:50 -0500 Subject: [PATCH 01/27] feat: post-deploy v2 prep (#96) - solc 0.8.26 - latest common - latest forge-std - registrar as constructor arg - excess destination as constructor arg - constants made public - token name changed - `IsApprovedEarner` and `NotApprovedEarner` errors have account as parameter - basic Migrator contract introduced - version bump - use more from common - more test coverage - fixed scripts to allow for generic deploy and mainnet upgrade --- .env.deploy.example | 15 + .env.example | 8 +- .env.upgrade.example | 8 + .prettierrc | 2 +- .solhint.json | 2 +- Makefile | 7 +- foundry.toml | 8 +- lib/common | 2 +- lib/forge-std | 2 +- package.json | 2 +- script/Deploy.s.sol | 76 +++ script/DeployBase.sol | 84 ++- script/DeployProduction.s.sol | 74 --- script/DeployUpgradeMainnet.s.sol | 85 +++ src/Migratable.sol | 61 -- src/MigratorV1.sol | 26 + src/Proxy.sol | 47 -- src/WrappedMToken.sol | 272 +++++--- src/interfaces/IMTokenLike.sol | 7 +- src/interfaces/IMigratable.sol | 44 -- src/interfaces/IRegistrarLike.sol | 5 +- src/interfaces/IWrappedMToken.sol | 69 +- src/libs/IndexingMath.sol | 97 --- test/Migration.t.sol | 102 --- test/integration/Deploy.t.sol | 37 +- test/integration/MorphoBlue.t.sol | 622 +++--------------- test/integration/Protocol.t.sol | 459 +++++++------ test/integration/TestBase.sol | 82 ++- test/integration/UniswapV3.t.sol | 475 +++++-------- test/integration/Upgrade.t.sol | 55 ++ .../vendor/morpho-blue/Interfaces.sol | 54 +- .../vendor/morpho-blue/MorphoTestBase.sol | 215 ++++++ .../vendor/protocol/Interfaces.sol | 2 +- .../vendor/uniswap-v3/Interfaces.sol | 2 +- test/integration/vendor/uniswap-v3/Utils.sol | 5 +- test/unit/Migration.t.sol | 75 +++ test/{ => unit}/Stories.t.sol | 52 +- test/{ => unit}/WrappedMToken.t.sol | 557 +++++++++++----- test/utils/Invariants.sol | 5 +- test/utils/Mocks.sol | 14 +- test/utils/WrappedMTokenHarness.sol | 9 +- 41 files changed, 1896 insertions(+), 1929 deletions(-) create mode 100644 .env.deploy.example create mode 100644 .env.upgrade.example create mode 100644 script/Deploy.s.sol delete mode 100644 script/DeployProduction.s.sol create mode 100644 script/DeployUpgradeMainnet.s.sol delete mode 100644 src/Migratable.sol create mode 100644 src/MigratorV1.sol delete mode 100644 src/Proxy.sol delete mode 100644 src/interfaces/IMigratable.sol delete mode 100644 src/libs/IndexingMath.sol delete mode 100644 test/Migration.t.sol create mode 100644 test/integration/Upgrade.t.sol create mode 100644 test/integration/vendor/morpho-blue/MorphoTestBase.sol create mode 100644 test/unit/Migration.t.sol rename test/{ => unit}/Stories.t.sol (90%) rename test/{ => unit}/WrappedMToken.t.sol (69%) diff --git a/.env.deploy.example b/.env.deploy.example new file mode 100644 index 0000000..3453c66 --- /dev/null +++ b/.env.deploy.example @@ -0,0 +1,15 @@ +# 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 +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/.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..61be6b6 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 : diff --git a/foundry.toml b/foundry.toml index 016467e..b014462 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,15 +2,13 @@ gas_reports = ["*"] gas_reports_ignore = [] ignored_error_codes = [] -solc_version = "0.8.23" +solc_version = "0.8.26" optimizer = true optimizer_runs = 999999 verbosity = 3 -block_number = 20_270_778 -block_timestamp = 1_720_550_400 -fork_block_number = 20_270_778 +fork_block_number = 21_345_650 rpc_storage_caching = { chains = ["mainnet"], endpoints = "all" } -evm_version = "shanghai" +evm_version = "cancun" [profile.production] build_info = true diff --git a/lib/common b/lib/common index 45aa01d..3692db1 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 45aa01db3acf6189e6b26ada5cd5ac8351837b56 +Subproject commit 3692db150ad90b21d7c213ea535f34792ad8873f diff --git a/lib/forge-std b/lib/forge-std index bf66061..b93cf4b 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 +Subproject commit b93cf4bc34ff214c099dc970b153f85ade8c9f66 diff --git a/package.json b/package.json index 2082010..e5826db 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": { diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..4bfec93 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,76 @@ +// 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 deployerProxyNonce_ = uint64(vm.envUint("DEPLOYER_PROXY_NONCE")); + + address expectedProxy_ = vm.envAddress("EXPECTED_PROXY"); + + console2.log("Deployer:", deployer_); + + if (deployer_ != expectedDeployer_) revert DeployerMismatch(expectedDeployer_, deployer_); + + uint64 currentNonce_ = vm.getNonce(deployer_); + + uint64 startNonce_ = currentNonce_; + address implementation_; + address proxy_; + + while (true) { + if (startNonce_ > deployerProxyNonce_) revert DeployerNonceTooHigh(); + + (implementation_, proxy_) = mockDeploy(deployer_, startNonce_); + + if (proxy_ == expectedProxy_) 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(); + + (implementation_, proxy_) = deploy( + vm.envAddress("M_TOKEN"), + vm.envAddress("REGISTRAR"), + vm.envAddress("EXCESS_DESTINATION"), + vm.envAddress("MIGRATION_ADMIN") + ); + + vm.stopBroadcast(); + + console2.log("Wrapped M Implementation address:", implementation_); + console2.log("Wrapped M Proxy address:", proxy_); + + if (proxy_ != expectedProxy_) revert ResultingProxyMismatch(expectedProxy_, proxy_); + } +} diff --git a/script/DeployBase.sol b/script/DeployBase.sol index 857b601..c4e1325 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -1,57 +1,91 @@ // 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 { MigratorV1 } from "../src/MigratorV1.sol"; import { WrappedMToken } from "../src/WrappedMToken.sol"; -import { Proxy } from "../src/Proxy.sol"; contract DeployBase { /** * @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 excessDestination_ The address of the excess destination. + * @param migrationAdmin_ The address of 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. */ function deploy( address mToken_, + address registrar_, + address excessDestination_, address migrationAdmin_ ) public virtual returns (address implementation_, address proxy_) { - // Wrapped M token needs `mToken_` and `migrationAdmin_` addresses. + // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. // Proxy needs `implementation_` addresses. - implementation_ = address(new WrappedMToken(mToken_, migrationAdmin_)); + implementation_ = address(new WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_)); proxy_ = address(new Proxy(implementation_)); } - 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 migrationAdmin_ The address of the Migration Admin. + * @return implementation_ The address of the deployed Wrapped M Token implementation. + * @return migrator_ The address of the deployed Migrator. + */ + function deployUpgrade( + address mToken_, + address registrar_, + address excessDestination_, + address migrationAdmin_ + ) public virtual returns (address implementation_, address migrator_) { + // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. + // Migrator needs `implementation_` addresses. + + implementation_ = address(new WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_)); + migrator_ = address(new MigratorV1(implementation_)); } - function getExpectedWrappedMTokenImplementation( + /** + * @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 implementation_ The address of the would-be Wrapped M Token implementation. + * @return proxy_ The address of the would-be Wrapped M Token proxy. + */ + function mockDeploy( address deployer_, uint256 deployerNonce_ - ) public pure virtual returns (address) { - return _getExpectedWrappedMTokenImplementation(deployer_, deployerNonce_); - } + ) public view virtual returns (address implementation_, address proxy_) { + // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. + // Proxy needs `implementation_` addresses. - function _getExpectedWrappedMTokenProxy(address deployer_, uint256 deployerNonce_) internal pure returns (address) { - return ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); + implementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + proxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); } - 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 implementation_ The address of the would-be Wrapped M Token implementation. + * @return migrator_ The address of the would-be Migrator. + */ + function mockDeployUpgrade( address deployer_, uint256 deployerNonce_ - ) public pure virtual returns (address) { - return _getExpectedWrappedMTokenProxy(deployer_, deployerNonce_); - } + ) public view virtual returns (address implementation_, address migrator_) { + // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. + // Migrator needs `implementation_` addresses. - function getDeployerNonceAfterProtocolDeployment(uint256 deployerNonce_) public pure virtual returns (uint256) { - return deployerNonce_ + 2; + implementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + migrator_ = 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..060e563 --- /dev/null +++ b/script/DeployUpgradeMainnet.s.sol @@ -0,0 +1,85 @@ +// 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 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 _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; + + // NOTE: Ensure this is the correct Migration Admin mainnet address. + address internal constant _MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + + address internal constant _PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; // Mainnet address for the Proxy. + + // 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_MIGRATOR = address(0); + + 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 implementation_; + address migrator_; + + while (true) { + if (startNonce_ > _DEPLOYER_MIGRATOR_NONCE) revert DeployerNonceTooHigh(); + + (implementation_, migrator_) = mockDeployUpgrade(deployer_, startNonce_); + + if (migrator_ == _EXPECTED_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(); + + (implementation_, migrator_) = deployUpgrade(_M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _MIGRATION_ADMIN); + + vm.stopBroadcast(); + + console2.log("Wrapped M Implementation address:", implementation_); + console2.log("Migrator address:", migrator_); + + if (migrator_ != _EXPECTED_MIGRATOR) revert ResultingMigratorMismatch(_EXPECTED_MIGRATOR, migrator_); + } +} 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/MigratorV1.sol b/src/MigratorV1.sol new file mode 100644 index 0000000..74cef78 --- /dev/null +++ b/src/MigratorV1.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.26; + +/** + * @title Migrator contract for migrating a WrappedMToken contract from V1 to V2. + * @author M^0 Labs + */ +contract MigratorV1 { + /// @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; + + constructor(address newImplementation_) { + newImplementation = newImplementation_; + } + + fallback() external virtual { + address newImplementation_ = newImplementation; + + assembly { + sstore(_IMPLEMENTATION_SLOT, newImplementation_) + } + } +} 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..096231c 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -1,20 +1,19 @@ // 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 { 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 { IWrappedMToken } from "./interfaces/IWrappedMToken.sol"; -import { Migratable } from "./Migratable.sol"; - /* ██╗ ██╗██████╗ █████╗ ██████╗ ██████╗ ███████╗██████╗ ███╗ ███╗ ████████╗ ██████╗ ██╗ ██╗███████╗███╗ ██╗ @@ -31,25 +30,35 @@ import { Migratable } from "./Migratable.sol"; * @author M^0 Labs */ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { + /* ============ 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 lastIndex The index of the last interaction for the account (0 for non-earning accounts). + */ struct Account { + // First Slot bool isEarning; uint240 balance; + // Second slot uint128 lastIndex; } /* ============ 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 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,7 +70,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { address public immutable registrar; /// @inheritdoc IWrappedMToken - address public immutable vault; + address public immutable excessDestination; /// @inheritdoc IWrappedMToken uint112 public principalOfTotalEarningSupply; @@ -83,15 +92,21 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /** * @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 migrationAdmin_ The address of a migration admin. + * @param mToken_ The address of an M Token. + * @param registrar_ The address of a Registrar. + * @param excessDestination_ The address of an excess destination. + * @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 excessDestination_, + address migrationAdmin_ + ) ERC20Extended("M (Wrapped) by M^0", "wM", 6) { if ((mToken = mToken_) == address(0)) revert ZeroMToken(); + if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); + if ((excessDestination = excessDestination_) == address(0)) revert ZeroExcessDestination(); if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); - - registrar = IMTokenLike(mToken_).ttgRegistrar(); - vault = IRegistrarLike(registrar).vault(); } /* ============ Interactive Functions ============ */ @@ -103,7 +118,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function wrap(address recipient_) external returns (uint240 wrapped_) { - return _wrap(msg.sender, recipient_, UIntMath.safe240(IMTokenLike(mToken).balanceOf(msg.sender))); + return _wrap(msg.sender, recipient_, _mBalanceOf(msg.sender)); } /// @inheritdoc IWrappedMToken @@ -125,7 +140,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function claimExcess() external returns (uint240 excess_) { emit ExcessClaimed(excess_ = excess()); - IMTokenLike(mToken).transfer(vault, excess_); + IMTokenLike(mToken).transfer(excessDestination, excess_); } /// @inheritdoc IWrappedMToken @@ -164,55 +179,15 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @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_; - - uint240 balance_ = accountInfo_.balance; - - _addTotalEarningSupply(balance_, currentIndex_); - - unchecked { - totalNonEarningSupply -= balance_; - } - - emit StartedEarning(account_); + _startEarningFor(account_, _currentMIndex()); } /// @inheritdoc IWrappedMToken function stopEarningFor(address account_) external { - _revertIfApprovedEarner(account_); - - uint128 currentIndex_ = currentIndex(); - - _claim(account_, currentIndex_); - - Account storage accountInfo_ = _accounts[account_]; - - if (!accountInfo_.isEarning) return; - - accountInfo_.isEarning = false; - accountInfo_.lastIndex = 0; - - uint240 balance_ = accountInfo_.balance; - - _subtractTotalEarningSupply(balance_, currentIndex_); - - unchecked { - totalNonEarningSupply += balance_; - } - - emit StoppedEarning(account_); + _stopEarningFor(account_, currentIndex()); } /* ============ Temporary Admin Migration ============ */ @@ -230,9 +205,8 @@ 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_.lastIndex, currentIndex()) : 0; } /// @inheritdoc IERC20 @@ -242,11 +216,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function balanceWithYieldOf(address account_) public view returns (uint256 balance_) { - return balanceOf(account_) + accruedYieldOf(account_); + unchecked { + return balanceOf(account_) + accruedYieldOf(account_); + } } /// @inheritdoc IWrappedMToken - function lastIndexOf(address account_) public view returns (uint128 lastIndex_) { + function lastIndexOf(address account_) external view returns (uint128 lastIndex_) { return _accounts[account_].lastIndex; } @@ -255,9 +231,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return address( uint160( - uint256( - IRegistrarLike(registrar).get(keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_PREFIX, account_))) - ) + uint256(_getFromRegistrar(keccak256(abi.encode(CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, account_)))) ) ); } @@ -284,9 +258,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function excess() public view returns (uint240 excess_) { + uint128 currentIndex_ = currentIndex(); + uint240 balance_ = _mBalanceOf(address(this)); + unchecked { - uint128 currentIndex_ = currentIndex(); - uint240 balance_ = uint240(IMTokenLike(mToken).balanceOf(address(this))); uint240 earmarked_ = totalNonEarningSupply + _projectedEarningSupply(currentIndex_); return balance_ > earmarked_ ? _getSafeTransferableM(balance_ - earmarked_, currentIndex_) : 0; @@ -305,7 +280,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IERC20 function totalSupply() external view returns (uint256 totalSupply_) { - return totalEarningSupply + totalNonEarningSupply; + unchecked { + return totalEarningSupply + totalNonEarningSupply; + } } /* ============ Internal Interactive Functions ============ */ @@ -374,13 +351,13 @@ 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_]; + 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_); + unchecked { accountInfo_.balance = balance_ - amount_; totalNonEarningSupply -= amount_; } @@ -396,8 +373,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { // 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_); } + + _addTotalEarningSupply(amount_, currentIndex_); } /** @@ -407,16 +385,17 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @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_); + unchecked { accountInfo_.balance = balance_ - amount_; - _subtractTotalEarningSupply(amount_, currentIndex_); } + + _subtractTotalEarningSupply(amount_, currentIndex_); } /** @@ -440,9 +419,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { accountInfo_.lastIndex = currentIndex_; - unchecked { - if (yield_ == 0) return 0; + if (yield_ == 0) return 0; + unchecked { accountInfo_.balance = startingBalance_ + yield_; // Update the total earning supply to account for the yield, but the principal has not changed. @@ -479,6 +458,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { emit Transfer(sender_, recipient_, amount_); + if (amount_ == 0) return; + Account storage senderAccountInfo_ = _accounts[sender_]; Account storage recipientAccountInfo_ = _accounts[recipient_]; @@ -536,15 +517,15 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { */ function _subtractTotalEarningSupply(uint240 amount_, uint128 currentIndex_) internal { if (amount_ >= totalEarningSupply) { - totalEarningSupply = 0; - principalOfTotalEarningSupply = 0; + delete totalEarningSupply; + delete principalOfTotalEarningSupply; return; } - unchecked { - uint112 principal_ = IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_); + uint112 principal_ = IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_); + unchecked { principalOfTotalEarningSupply -= ( principal_ > principalOfTotalEarningSupply ? principalOfTotalEarningSupply : principal_ ); @@ -561,7 +542,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @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)); + uint240 startingBalance_ = _mBalanceOf(address(this)); // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. IMTokenLike(mToken).transferFrom(account_, address(this), amount_); @@ -569,8 +550,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { // 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_)); + // starting and ending M balance is minted as WrappedM token. + _mint(recipient_, wrapped_ = _mBalanceOf(address(this)) - startingBalance_); } /** @@ -583,7 +564,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function _unwrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 unwrapped_) { _burn(account_, amount_); - uint256 startingBalance_ = IMTokenLike(mToken).balanceOf(address(this)); + uint240 startingBalance_ = _mBalanceOf(address(this)); // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. IMTokenLike(mToken).transfer(recipient_, _getSafeTransferableM(amount_, currentIndex())); @@ -592,7 +573,61 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { // 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))); + return startingBalance_ - _mBalanceOf(address(this)); + } + + /** + * @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 { + _revertIfNotApprovedEarner(account_); + + Account storage accountInfo_ = _accounts[account_]; + + if (accountInfo_.isEarning) return; + + accountInfo_.isEarning = true; + accountInfo_.lastIndex = currentIndex_; + + uint240 balance_ = accountInfo_.balance; + + _addTotalEarningSupply(balance_, currentIndex_); + + unchecked { + totalNonEarningSupply -= balance_; + } + + emit StartedEarning(account_); + } + + /** + * @dev Stops earning for `account` if disallowed by the Registrar. + * @param account_ The account to stop earning for. + * @param currentIndex_ The current index. + */ + function _stopEarningFor(address account_, uint128 currentIndex_) internal { + _revertIfApprovedEarner(account_); + + _claim(account_, currentIndex_); + + Account storage accountInfo_ = _accounts[account_]; + + if (!accountInfo_.isEarning) return; + + delete accountInfo_.isEarning; + delete accountInfo_.lastIndex; + + uint240 balance_ = accountInfo_.balance; + + _subtractTotalEarningSupply(balance_, currentIndex_); + + unchecked { + totalNonEarningSupply += balance_; + } + + emit StoppedEarning(account_); } /* ============ Internal View/Pure Functions ============ */ @@ -629,14 +664,23 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } } + /** + * @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 _getFromRegistrar(bytes32 key_) internal view returns (bytes32 value_) { + return IRegistrarLike(registrar).get(key_); + } + /** * @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 amount_ Some amount to be transferred out of this contract. * @param currentIndex_ The current index. * @return safeAmount_ The adjusted amount that can safely be transferred out. */ 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_`. + // If this contract is earning, adjust `amount_` to ensure it's M balance decrement is limited to `amount_`. return IMTokenLike(mToken).isEarning(address(this)) ? IndexingMath.getPresentAmountRoundedDown( @@ -652,20 +696,30 @@ 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. + * @dev Returns whether `account_` is a Registrar-approved earner. * @param account_ The account being queried. - * @return isApproved_ True if the account_ is a TTG-approved earner, false otherwise. + * @return isApproved_ True if the account_ is a Registrar-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_); + _getFromRegistrar(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || + IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, account_); + } + + /** + * @dev Returns the M Token balance of `account_`. + * @param account_ The account being queried. + * @return balance_ The M Token balance of the account. + */ + 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 +740,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 +752,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,7 +760,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param account_ Address of an account. */ function _revertIfNotApprovedEarner(address account_) internal view { - if (!_isApprovedEarner(account_)) revert NotApprovedEarner(); + if (!_isApprovedEarner(account_)) revert NotApprovedEarner(account_); } /** diff --git a/src/interfaces/IMTokenLike.sol b/src/interfaces/IMTokenLike.sol index e0c7939..3d902bc 100644 --- a/src/interfaces/IMTokenLike.sol +++ b/src/interfaces/IMTokenLike.sol @@ -1,6 +1,6 @@ // 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. @@ -26,7 +26,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. @@ -50,7 +50,4 @@ 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); } 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..2981059 100644 --- a/src/interfaces/IRegistrarLike.sol +++ b/src/interfaces/IRegistrarLike.sol @@ -1,6 +1,6 @@ // 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. @@ -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/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index f684adb..a74ca47 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -1,10 +1,9 @@ // 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. @@ -22,19 +21,19 @@ 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. + * @notice Emitted when Wrapped M earning is enabled. * @param index The index at the moment earning is enabled. */ event EarningEnabled(uint128 index); /** - * @notice Emitted when earning is disabled for the entire wrapper. + * @notice Emitted when Wrapped M earning is disabled. * @param index The 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); @@ -62,8 +61,11 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @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 earner by the Registrar. + * @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,18 +75,27 @@ 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 earner by the Registrar. + * @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. error UnauthorizedMigration(); + /// @notice Emitted in constructor 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(); + /* ============ Interactive Functions ============ */ /** @@ -125,25 +136,25 @@ interface IWrappedMToken is IMigratable, IERC20Extended { function claimFor(address account) external returns (uint240 yield); /** - * @notice Claims any excess M of the wrapper. + * @notice Claims any excess M of this contract. * @return excess The amount of excess claimed. */ function claimExcess() external returns (uint240 excess); - /// @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 if it has never been done. 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 if it has never been done. 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 Stops earning for `account` if disallowed by the Registrar. * @param account The account to stop earning for. */ function stopEarningFor(address account) external; @@ -158,6 +169,18 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /* ============ View/Pure Functions ============ */ + /// @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. @@ -186,10 +209,10 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ function claimOverrideRecipientFor(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. + /// @notice This contract's current excess M that is not earmarked for account balances or accrued yield. function excess() external view returns (uint240 excess); /** @@ -199,19 +222,19 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ 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. + /// @notice Whether Wrapped M earning has been enabled at least once. 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 address of the Registrar. function registrar() external view returns (address registrar); /// @notice The portion of total supply that is not earning yield. @@ -226,6 +249,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The principal of totalEarningSupply to help compute totalAccruedYield(), and thus excess(). function principalOfTotalEarningSupply() external view returns (uint112 principalOfTotalEarningSupply); - /// @notice The address of the vault where excess is claimed to. - function vault() external view returns (address vault); + /// @notice The address of the destination where excess is claimed to. + function excessDestination() external view returns (address excessDestination); } 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/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/integration/Deploy.t.sol b/test/integration/Deploy.t.sol index 79aebfc..ced7573 100644 --- a/test/integration/Deploy.t.sol +++ b/test/integration/Deploy.t.sol @@ -1,45 +1,44 @@ // 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 _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; - uint256 internal constant _DEPLOYER_PROXY_NONCE = 40; - address internal constant _EXPECTED_PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; + + uint64 internal constant _DEPLOYER_NONCE = 50; 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 expectedImplementation_, address expectedProxy_) = mockDeploy(_DEPLOYER, _DEPLOYER_NONCE); vm.startPrank(_DEPLOYER); - (address implementation_, address proxy_) = deploy(_M_TOKEN, _MIGRATION_ADMIN); + (address implementation_, address proxy_) = deploy(_M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _MIGRATION_ADMIN); vm.stopPrank(); // Wrapped M Token Implementation assertions - assertEq(implementation_, getExpectedWrappedMTokenImplementation(_DEPLOYER, 39)); + assertEq(implementation_, expectedImplementation_); 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()); + assertEq(IWrappedMToken(implementation_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(implementation_).excessDestination(), _EXCESS_DESTINATION); - // // Wrapped M Token Proxy assertions - assertEq(proxy_, getExpectedWrappedMTokenProxy(_DEPLOYER, 39)); - assertEq(proxy_, _EXPECTED_PROXY); + // Wrapped M Token Proxy assertions + assertEq(proxy_, expectedProxy_); 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(IWrappedMToken(proxy_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(proxy_).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(proxy_).implementation(), implementation_); } } diff --git a/test/integration/MorphoBlue.t.sol b/test/integration/MorphoBlue.t.sol index 8f38a88..36ec030 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,231 @@ 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; - function setUp() public override { - super.setUp(); - - _addToList(_EARNERS_LIST, address(_wrappedMToken)); + uint240 internal _excess; - _wrappedMToken.enableEarning(); + function setUp() external { + _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 and the yield has been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= _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 += 49_292101); // 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 and the yield has been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= _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 += 121446); // 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 and the yield has been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= _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_994258); // 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 and the yield has been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= _morphoAccruedYield); - 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 += 77193); // 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..dffe6c6 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -1,6 +1,6 @@ // 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"; @@ -22,26 +22,44 @@ contract ProtocolIntegrationTests is TestBase { uint256 internal _carolAccruedYield; uint256 internal _daveAccruedYield; + uint256 internal _totalEarningSupply; + uint256 internal _totalNonEarningSupply; + uint256 internal _totalSupply; + uint256 internal _totalAccruedYield; uint256 internal _excess; - function setUp() public override { - super.setUp(); - - _addToList(_EARNERS_LIST, address(_wrappedMToken)); - _addToList(_EARNERS_LIST, _alice); - _addToList(_EARNERS_LIST, _bob); - - _wrappedMToken.enableEarning(); + function setUp() external { + _addToList(_EARNERS_LIST_NAME, _alice); + _addToList(_EARNERS_LIST_NAME, _bob); _wrappedMToken.startEarningFor(_alice); _wrappedMToken.startEarningFor(_bob); + _deployV2Components(); + _migrate(); + _totalEarningSupplyOfM = _mToken.totalEarningSupply(); + + _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); + + _totalEarningSupply = _wrappedMToken.totalEarningSupply(); + _totalNonEarningSupply = _wrappedMToken.totalNonEarningSupply(); + _totalAccruedYield = _wrappedMToken.totalAccruedYield(); + _excess = _wrappedMToken.excess(); } - function test_initialState() external view { + 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 M^0"); + assertEq(_wrappedMToken.symbol(), "wM"); + assertEq(_wrappedMToken.decimals(), 6); + } + + function test_state() external view { assertEq(_mToken.currentIndex(), _wrappedMToken.currentIndex()); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), 0); assertTrue(_mToken.isEarning(address(_wrappedMToken))); } @@ -53,7 +71,7 @@ contract ProtocolIntegrationTests is TestBase { _wrap(_alice, _alice, 100_000000); // Assert M Token - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM = 99_999999); + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 99_999999); assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 99_999999); // Assert Alice (Earner) @@ -61,16 +79,12 @@ contract ProtocolIntegrationTests is TestBase { 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 += 99_999999); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); - assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); _giveM(_carol, 50_000000); @@ -86,45 +100,36 @@ contract ProtocolIntegrationTests is TestBase { 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 - ); + assertGe(_wrapperBalanceOfM, _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_190592); // 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 - ); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); _giveM(_bob, 200_000000); @@ -141,16 +146,12 @@ contract ProtocolIntegrationTests is TestBase { 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.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 199_999999); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); - assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); _giveM(_dave, 150_000000); @@ -159,24 +160,20 @@ contract ProtocolIntegrationTests is TestBase { _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.balanceOf(_dave), _daveBalance = 149_999999); 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.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 149_999999); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); - assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); assertEq(_wrappedMToken.claimFor(_alice), _aliceAccruedYield); @@ -186,36 +183,33 @@ 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_190592); // 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_190592); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1_190592); assertEq(_wrappedMToken.excess(), _excess); - assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _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_423880); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield = 4_992808); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield = 4_790723); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -226,16 +220,10 @@ 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); - - assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); } function test_integration_yieldTransfer() external { @@ -257,15 +245,27 @@ contract ProtocolIntegrationTests is TestBase { 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); + + assertGe(_wrapperBalanceOfM, _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_395361); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -274,43 +274,39 @@ 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); // 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); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = _aliceBalance + 2_395361 - 100_000000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 2_395361); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += 100_000000); 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); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _bobBalance + 2_395361 - 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + 100_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361 - 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); - assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); _transferWM(_dave, _bob, 50_000000); @@ -323,30 +319,30 @@ 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.excess(), _excess); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 50_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 50_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); - assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _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 += 57376); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 3_744606); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 3_593042); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -357,79 +353,92 @@ 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); - - assertGe( - _wrapperBalanceOfM, - _aliceBalance + _aliceAccruedYield + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); } function test_integration_yieldClaimUnwrap() external { _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 += 99_999999); + assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += 100_000000); + + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 199_999999); + + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 99_999999); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); + + assertGe(_wrapperBalanceOfM, _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_395361); + + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance += 100_000000); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance += 100_000000); + + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 200_000000); + + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); + + assertGe(_wrapperBalanceOfM, _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_219112); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_190593); // 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_614473); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 3_614473); // 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 -= 99_999999); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _aliceBalance); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614473); + assertEq(_wrappedMToken.excess(), _excess); - assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _aliceBalance + _bobBalance + _bobAccruedYield + _carolBalance + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); // Start earning for Carol - _addToList(_EARNERS_LIST, _carol); + _addToList(_EARNERS_LIST_NAME, _carol); _wrappedMToken.startEarningFor(_carol); @@ -438,128 +447,104 @@ 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 - ); + assertGe(_wrapperBalanceOfM, _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_423881); + assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield += 2_395361); - assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _aliceBalance + _bobBalance + _bobAccruedYield + _carolBalance + _carolAccruedYield + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _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); + assertEq(_mToken.balanceOf(_alice), 103_614471); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= 103_614472); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 199_999999); - assertEq(_wrappedMToken.totalNonEarningSupply(), 99_999999); - assertEq(_wrappedMToken.totalSupply(), 299_999998); - assertEq(_wrappedMToken.totalAccruedYield(), 6_264288); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 103_614472); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess += 1); - assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _bobBalance + _bobAccruedYield + _carolBalance + _carolAccruedYield + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _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); + assertEq(_mToken.balanceOf(_bob), 100_000000 + 3_614474 - 1); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= 100_000000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= 3_614474); // 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 -= 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614474); + assertEq(_wrappedMToken.excess(), _excess += 1); - assertGe( - _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), - _carolBalance + _carolAccruedYield + _daveBalance + _excess - ); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); // Accrued yield of Carol is claimed when unwrapping _unwrap(_carol, _carol, _carolBalance + _carolAccruedYield); // Assert Carol (Earner) - assertEq(_mToken.balanceOf(_carol), _carolBalance + _carolAccruedYield - 1); - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance -= _carolBalance); - assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield -= _carolAccruedYield); + assertEq(_mToken.balanceOf(_carol), 100_000000 + 2_395361 - 1); + assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance -= 100_000000); + assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield -= 2_395361); // 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); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361); + assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), _daveBalance + _excess); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); _unwrap(_dave, _dave, _daveBalance); // Assert Dave (Non-Earner) - assertEq(_mToken.balanceOf(_dave), _daveBalance - 1); - assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance -= _daveBalance); + assertEq(_mToken.balanceOf(_dave), 100_000000 - 1); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance -= 100_000000); 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); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 100_000000); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), _excess); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); - uint256 vaultStartingBalance_ = _mToken.balanceOf(_vault); + uint256 vaultStartingBalance_ = _mToken.balanceOf(_excessDestination); assertEq(_wrappedMToken.claimExcess(), _excess); - assertEq(_mToken.balanceOf(_vault), _excess + vaultStartingBalance_); + assertEq(_mToken.balanceOf(_excessDestination), _excess + vaultStartingBalance_); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalSupply(), 0); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess -= _excess); - assertGe(_wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)), 0); + assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); } function testFuzz_full(uint256 seed_) external { + // TODO: Reinstate to test post-migration for new version. vm.skip(true); for (uint256 index_; index_ < _accounts.length; ++index_) { @@ -567,12 +552,22 @@ contract ProtocolIntegrationTests is TestBase { } for (uint256 index_; index_ < 1000; ++index_) { + 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("--------"); + // console2.log(""); uint256 timeDelta_ = (seed_ = _getNewSeed(seed_)) % 30 days; + // console2.log("Warping %s hours", timeDelta_ / 1 hours); + 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.checkInvariant4(address(_wrappedMToken), _accounts), "Invariant 4 Failed."); @@ -580,8 +575,6 @@ contract ProtocolIntegrationTests is TestBase { // 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."); - // console2.log(""); - // console2.log("--------"); // console2.log("Wrapper has %s M", _mToken.balanceOf(address(_wrappedMToken))); address account1_ = _accounts[((seed_ = _getNewSeed(seed_)) % _accounts.length)]; @@ -658,13 +651,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..f51601c 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -1,29 +1,48 @@ // 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 { IMTokenLike, IRegistrarLike } from "./vendor/protocol/Interfaces.sol"; +import { Test } from "../../lib/forge-std/src/Test.sol"; -import { Proxy } from "../../src/Proxy.sol"; +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; +import { MigratorV1 } from "../../src/MigratorV1.sol"; + +import { IMTokenLike, IRegistrarLike } from "./vendor/protocol/Interfaces.sol"; contract TestBase is Test { 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 = 0x563AA56D0B627d1A734e04dF5762F5Eea1D56C2f; + address internal constant _wmSource = 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D; + + IWrappedMToken internal constant _wrappedMToken = IWrappedMToken(0x437cc33344a0B27A429f795ff6B469C72698B291); - bytes32 internal constant _EARNERS_LIST = "earners"; + 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"; + + // USDC on Ethereum Mainnet + address internal constant _USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + // Large USDC holder on Ethereum Mainnet + address internal constant _USDC_SOURCE = 0x4B16c5dE96EB2117bBE5fd171E4d203624B014aa; - address internal _wrappedMTokenImplementation; + // DAI on Ethereum Mainnet + address internal constant _DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; - WrappedMToken internal _wrappedMToken; + // Large DAI holder on Ethereum Mainnet + address internal constant _DAI_SOURCE = 0xD1668fB5F690C59Ab4B0CAbAd0f8C1617895052B; + + address internal _migrationAdmin = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal _alice = makeAddr("alice"); address internal _bob = makeAddr("bob"); @@ -38,13 +57,20 @@ contract TestBase is Test { address[] internal _accounts = [_alice, _bob, _carol, _dave, _eric, _frank, _grace, _henry, _ivan, _judy]; - address internal _migrationAdmin = makeAddr("migrationAdmin"); + address internal _implementationV2; + address internal _migratorV1; - bytes32 internal constant _CLAIM_OVERRIDE_RECIPIENT_PREFIX = "wm_claim_override_recipient"; + function _getSource(address token_) internal pure returns (address source_) { + if (token_ == _USDC) return _USDC_SOURCE; + + if (token_ == _DAI) return _DAI_SOURCE; - function setUp() public virtual { - _wrappedMTokenImplementation = address(new WrappedMToken(address(_mToken), _migrationAdmin)); - _wrappedMToken = WrappedMToken(address(new Proxy(_wrappedMTokenImplementation))); + 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 +78,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,6 +88,11 @@ 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_); } @@ -110,4 +141,25 @@ 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 { + _implementationV2 = address( + new WrappedMToken(address(_mToken), _registrar, _excessDestination, _migrationAdmin) + ); + _migratorV1 = address(new MigratorV1(_implementationV2)); + } + + function _migrate() internal { + _set( + keccak256(abi.encode(_MIGRATOR_V1_PREFIX, address(_wrappedMToken))), + bytes32(uint256(uint160(_migratorV1))) + ); + + _wrappedMToken.migrate(); + } + + function _migrateFromAdmin() internal { + vm.prank(_migrationAdmin); + _wrappedMToken.migrate(_migratorV1); + } } diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index 0c3a05d..e06721d 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,136 +50,79 @@ contract UniswapV3IntegrationTests is TestBase { uint256 internal _poolAccruedYield; uint256 internal _bobAccruedYield; - function setUp() public override { - super.setUp(); + uint240 internal _excess; - _addToList(_EARNERS_LIST, address(_wrappedMToken)); + function setUp() external { + _deployV2Components(); + _migrate(); - _wrappedMToken.enableEarning(); + _poolClaimRecipient = _wrappedMToken.claimOverrideRecipientFor(_pool); - _pool = _createPool(); + _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_initialState() external view { + function test_state() external view { assertTrue(_mToken.isEarning(address(_wrappedMToken))); - assertEq(_wrappedMToken.isEarningEnabled(), true); - assertFalse(_wrappedMToken.isEarning(_pool)); - } - - 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 + 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); + _giveWM(_alice, 1_001e6); - _mintNewPosition(_alice, _alice, 1_000_000e6); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_001e6); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); + _give(_USDC, _alice, 1_001e6); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC = 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_001e6); - /* ============ Pool Becomes An Earner ============ */ + _mintNewPosition(_alice, _alice, 1_000e6); - _setClaimOverrideRecipient(_pool, _carol); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 999_930937); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); - _addToList(_EARNERS_LIST, _pool); - _wrappedMToken.startEarningFor(_pool); + // The mint has triggered a wM transfer and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); - assertTrue(_wrappedMToken.isEarning(_pool)); - - assertEq(_wrappedMToken.claimOverrideRecipientFor(_pool), _carol); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); - 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 += 878_557_430309); // 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 and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM -= swapAmountOut_); assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); @@ -190,80 +132,58 @@ contract UniswapV3IntegrationTests is TestBase { // 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 += 878_508_264721); // 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 and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 100_000e6); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _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_); - - assertApproxEqAbs(_wrappedMToken.balanceOf(_alice), 0, 10); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += aliceAmount_); + _giveWM(_alice, _aliceBalanceOfWM += aliceAmount_); - assertEq(IERC20(_USDC).balanceOf(_alice), 0); - assertEq(IERC20(_USDC).balanceOf(_pool), aliceAmount_); + _give(_USDC, _alice, _aliceBalanceOfUSDC += aliceAmount_); - /* ============ Pool Becomes An Earner ============ */ + (, , uint256 amount0_, uint256 amount1_) = _mintNewPosition(_alice, _alice, aliceAmount_); - _setClaimOverrideRecipient(_pool, _carol); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= amount0_); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += amount0_); - _addToList(_EARNERS_LIST, _pool); - _wrappedMToken.startEarningFor(_pool); + // The mint has triggered a wM transfer and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); - // Check that the pool is earning WM - assertTrue(_wrappedMToken.isEarning(_pool)); - - assertEq(_wrappedMToken.claimOverrideRecipientFor(_pool), _carol); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); - 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,13 +194,6 @@ 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); @@ -290,25 +203,22 @@ contract UniswapV3IntegrationTests is TestBase { 10 ); - // No excess yield has accrued in the wrapped M contract since the pool is the only earner. - assertApproxEqAbs(_wrappedMToken.excess(), 0, 10); - // _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 and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM -= swapOutWM_); assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); @@ -322,13 +232,6 @@ 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); @@ -339,109 +242,81 @@ contract UniswapV3IntegrationTests is TestBase { ); // 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 and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); 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)); - - assertEq(_wrappedMToken.claimOverrideRecipientFor(_pool), _carol); - /* ============ Alice Mints New LP Position ============ */ - _giveM(_alice, 1_000_100e6); - _wrap(_alice, _alice, _mToken.balanceOf(_alice)); + _giveWM(_alice, 1_001e6); + + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_001e6); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM += 1_000_099_999999); + _give(_USDC, _alice, 1_001e6); - deal(_USDC, _alice, 1_000_100e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_001e6); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC += 1_000_100e6); + _mintNewPosition(_alice, _alice, 1_000e6); - _mintNewPosition(_alice, _alice, 1_000_000e6); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 999_930937); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); + // The mint has triggered a wM transfer and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC = 1_000_000e6); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); - // 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 += 23_512_463128); /* ============ 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 += 1_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 100_000e6); + // The swap has triggered a wM transfer and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); - // Check that carol received yield. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); /* ============ 1-Day Time Warp ============ */ @@ -449,12 +324,13 @@ contract UniswapV3IntegrationTests is TestBase { // 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 += 2_349_986661); // 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,16 +338,17 @@ 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 += 11_753_024234); /* ============ 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); + + // The swap has triggered a wM transfer and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); - // Check that carol received yield. - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalanceOfWM += _poolAccruedYield); assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); /* ============ 3-Day Time Warp ============ */ @@ -479,17 +356,18 @@ contract UniswapV3IntegrationTests is TestBase { // 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 += 7_051_281772); /* ============ 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); + // The swap has triggered a wM transfer and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); /* ============ 7-Day Time Warp ============ */ @@ -497,139 +375,96 @@ contract UniswapV3IntegrationTests is TestBase { // 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 += 16_458_240233); /* ============ 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); + // The swap has triggered a wM transfer and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); } - function test_uniswapV3_increaseDecreaseLiquidityAndFees() public { - /* ============ Pool Becomes An Earner ============ */ - - _setClaimOverrideRecipient(_pool, _carol); - - _addToList(_EARNERS_LIST, _pool); - _wrappedMToken.startEarningFor(_pool); - - assertTrue(_wrappedMToken.isEarning(_pool)); - - assertEq(_wrappedMToken.claimOverrideRecipientFor(_pool), _carol); - + 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 -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 999_930937); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); - assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); - (uint256 bobTokenId_, , , ) = _mintNewPosition(_bob, _bob, 1_000_000e6); + (uint256 bobTokenId_, , , ) = _mintNewPosition(_bob, _bob, 1_000e6); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 1_000_000e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000_000e6); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 999_930937); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000_000e6); + assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000e6); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_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_317339); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_130_990918); /* ============ Dave (Non-Earner) Swaps Exact wM for USDC ============ */ - _giveM(_dave, 100_100e6); - _wrap(_dave, _dave, 100_100e6); - - assertEq(_wrappedMToken.balanceOf(_dave), _daveBalanceOfWM += 100_099_999999); - - _swapExactInput(_dave, _dave, address(_wrappedMToken), _USDC, 100_000e6); + _giveWM(_dave, 1_001e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= 95_229_024894); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalanceOfWM += 1_001e6); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 100_000e6); + _swapExactInput(_dave, _dave, address(_wrappedMToken), _USDC, 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(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= 999_903365); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += bobAmountWM_ + _bobAccruedYield); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM -= bobAmountWM_); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= _bobAccruedYield); + // The swap has triggered a wM transfer and the yield has been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); - assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC += bobAmountUSDC_); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= bobAmountUSDC_); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _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 +490,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..b8e1fd5 --- /dev/null +++ b/test/integration/Upgrade.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { Test } from "../../lib/forge-std/src/Test.sol"; + +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; + +import { DeployBase } from "../../script/DeployBase.sol"; + +contract UpgradeTests is Test, DeployBase { + address internal constant _WRAPPED_M_TOKEN = 0x437cc33344a0B27A429f795ff6B469C72698B291; + address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; + address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; + address internal constant _MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault + address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + uint64 internal constant _DEPLOYER_NONCE = 50; + + function test_upgrade() external { + vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); + + (address expectedImplementation_, address expectedMigrator_) = mockDeployUpgrade(_DEPLOYER, _DEPLOYER_NONCE); + + vm.startPrank(_DEPLOYER); + (address implementation_, address migrator_) = deployUpgrade( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _MIGRATION_ADMIN + ); + vm.stopPrank(); + + // Wrapped M Token Implementation assertions + assertEq(implementation_, expectedImplementation_); + assertEq(IWrappedMToken(implementation_).migrationAdmin(), _MIGRATION_ADMIN); + assertEq(IWrappedMToken(implementation_).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(implementation_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(implementation_).excessDestination(), _EXCESS_DESTINATION); + + // Migrator assertions + assertEq(migrator_, expectedMigrator_); + + vm.prank(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin()); + IWrappedMToken(_WRAPPED_M_TOKEN).migrate(migrator_); + + // Wrapped M Token Proxy assertions + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin(), _MIGRATION_ADMIN); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).implementation(), implementation_); + } +} 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..ccdcb04 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; 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/Migration.t.sol b/test/unit/Migration.t.sol new file mode 100644 index 0000000..6d81210 --- /dev/null +++ b/test/unit/Migration.t.sol @@ -0,0 +1,75 @@ +// 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 { MigratorV1 as Migrator } from "../../src/MigratorV1.sol"; + +import { MockRegistrar } from "./../utils/Mocks.sol"; + +contract WrappedMTokenV3 { + function foo() external pure returns (uint256) { + return 1; + } +} + +contract MigrationTests is Test { + bytes32 internal constant _MIGRATOR_KEY_PREFIX = "wm_migrator_v2"; + + 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 _excessDestination = makeAddr("excessDestination"); + address internal _migrationAdmin = makeAddr("migrationAdmin"); + + MockRegistrar internal _registrar; + WrappedMToken internal _implementation; + IWrappedMToken internal _wrappedMToken; + + function setUp() external { + _registrar = new MockRegistrar(); + + _implementation = new WrappedMToken(_mToken, address(_registrar), _excessDestination, _migrationAdmin); + + _wrappedMToken = IWrappedMToken(address(new Proxy(address(_implementation)))); + } + + function test_migration() external { + WrappedMTokenV3 implementationV3_ = new WrappedMTokenV3(); + address migrator_ = address(new Migrator(address(implementationV3_))); + + _registrar.set( + keccak256(abi.encode(_MIGRATOR_KEY_PREFIX, address(_wrappedMToken))), + bytes32(uint256(uint160(migrator_))) + ); + + vm.expectRevert(); + WrappedMTokenV3(address(_wrappedMToken)).foo(); + + _wrappedMToken.migrate(); + + assertEq(WrappedMTokenV3(address(_wrappedMToken)).foo(), 1); + } + + function test_migration_fromAdmin() external { + WrappedMTokenV3 implementationV3_ = new WrappedMTokenV3(); + address migrator_ = address(new Migrator(address(implementationV3_))); + + vm.expectRevert(); + WrappedMTokenV3(address(_wrappedMToken)).foo(); + + vm.prank(_migrationAdmin); + _wrappedMToken.migrate(migrator_); + + assertEq(WrappedMTokenV3(address(_wrappedMToken)).foo(), 1); + } +} diff --git a/test/Stories.t.sol b/test/unit/Stories.t.sol similarity index 90% rename from test/Stories.t.sol rename to test/unit/Stories.t.sol index 720daf0..906a3a0 100644 --- a/test/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -1,31 +1,31 @@ // 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 { 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 } 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 _excessDestination = makeAddr("excessDestination"); address internal _migrationAdmin = makeAddr("migrationAdmin"); - address internal _vault = makeAddr("vault"); - MockM internal _mToken; MockRegistrar internal _registrar; WrappedMToken internal _implementation; @@ -33,21 +33,19 @@ contract Tests is Test { 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); + _implementation = new WrappedMToken(address(_mToken), address(_registrar), _excessDestination, _migrationAdmin); _wrappedMToken = IWrappedMToken(address(new Proxy(address(_implementation)))); } 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(); @@ -243,7 +241,7 @@ contract Tests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 133_333336); assertEq(_wrappedMToken.excess(), 416_666664); - _registrar.setListContains(_EARNERS_LIST, _alice, false); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, false); _wrappedMToken.stopEarningFor(_alice); @@ -258,7 +256,7 @@ contract Tests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 66_666672); assertEq(_wrappedMToken.excess(), 416_666664); - _registrar.setListContains(_EARNERS_LIST, _carol, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); _wrappedMToken.startEarningFor(_carol); @@ -366,9 +364,10 @@ contract Tests is Test { } 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(); @@ -400,9 +399,10 @@ contract Tests is Test { } 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(); diff --git a/test/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol similarity index 69% rename from test/WrappedMToken.t.sol rename to test/unit/WrappedMToken.t.sol index 54fe412..53823ab 100644 --- a/test/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -1,42 +1,42 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; -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 { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; +import { UIntMath } from "../../lib/common/src/libs/UIntMath.sol"; -import { IWrappedMToken } from "../src/interfaces/IWrappedMToken.sol"; +import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; +import { IERC20Extended } from "../../lib/common/src/interfaces/IERC20Extended.sol"; -import { IndexingMath } from "../src/libs/IndexingMath.sol"; +import { Proxy } from "../../lib/common/src/Proxy.sol"; +import { Test } from "../../lib/forge-std/src/Test.sol"; -import { Proxy } from "../src/Proxy.sol"; +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { MockM, MockRegistrar } from "./utils/Mocks.sol"; -import { WrappedMTokenHarness } from "./utils/WrappedMTokenHarness.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: All operations involving earners should include demonstration of accrued yield being added to their balance. // TODO: Add relevant unit tests while earning enabled/disabled. +// TODO: Remove unneeded _wrappedMToken.enableEarning. contract WrappedMTokenTests is Test { - uint56 internal constant _EXP_SCALED_ONE = 1e12; + uint56 internal constant _EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; - bytes32 internal constant _EARNERS_LIST = "earners"; - bytes32 internal constant _CLAIM_DESTINATION_PREFIX = "wm_claim_destination"; - bytes32 internal constant _MIGRATOR_V1_PREFIX = "wm_migrator_v1"; + bytes32 internal constant _CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX = "wm_claim_override_recipient"; + + 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 _excessDestination = makeAddr("excessDestination"); address internal _migrationAdmin = makeAddr("migrationAdmin"); address[] internal _accounts = [_alice, _bob, _charlie, _david]; - address internal _vault = makeAddr("vault"); - uint128 internal _currentIndex; MockM internal _mToken; @@ -46,26 +46,37 @@ contract WrappedMTokenTests is Test { 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); + _implementation = new WrappedMTokenHarness( + address(_mToken), + address(_registrar), + _excessDestination, + _migrationAdmin + ); _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); _mToken.setCurrentIndex(_currentIndex = 1_100000068703); } + /* ============ 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.vault(), _vault); - assertEq(_wrappedMToken.name(), "WrappedM by M^0"); + assertEq(_wrappedMToken.excessDestination(), _excessDestination); + assertEq(_wrappedMToken.name(), "M (Wrapped) by M^0"); assertEq(_wrappedMToken.symbol(), "wM"); assertEq(_wrappedMToken.decimals(), 6); assertEq(_wrappedMToken.implementation(), address(_implementation)); @@ -73,12 +84,22 @@ contract WrappedMTokenTests is Test { function test_constructor_zeroMToken() external { vm.expectRevert(IWrappedMToken.ZeroMToken.selector); - new WrappedMTokenHarness(address(0), address(0)); + 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_zeroExcessDestination() external { + vm.expectRevert(IWrappedMToken.ZeroExcessDestination.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(0)); + new WrappedMTokenHarness(address(_mToken), address(_registrar), _excessDestination, address(0)); } function test_constructor_zeroImplementation() external { @@ -124,7 +145,7 @@ contract WrappedMTokenTests is Test { } function test_wrap_toEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -172,7 +193,7 @@ contract WrappedMTokenTests is Test { accountEarning_ = earningEnabled_ && accountEarning_; if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); } @@ -201,6 +222,9 @@ contract WrappedMTokenTests is Test { if (wrapAmount_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); + } else { + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, wrapAmount_); } vm.startPrank(_alice); @@ -227,7 +251,7 @@ contract WrappedMTokenTests is Test { accountEarning_ = earningEnabled_ && accountEarning_; if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); } @@ -256,6 +280,9 @@ contract WrappedMTokenTests is Test { if (wrapAmount_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); + } else { + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, wrapAmount_); } vm.startPrank(_alice); @@ -287,7 +314,7 @@ contract WrappedMTokenTests is Test { } function test_unwrap_insufficientBalance_fromEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -323,7 +350,7 @@ contract WrappedMTokenTests is Test { } function test_unwrap_fromEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -363,7 +390,7 @@ contract WrappedMTokenTests is Test { accountEarning_ = earningEnabled_ && accountEarning_; if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); } @@ -383,7 +410,6 @@ contract WrappedMTokenTests is Test { } currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - unwrapAmount_ = uint240(bound(unwrapAmount_, 0, 2 * balance_)); _mToken.setCurrentIndex(_currentIndex = currentIndex_); @@ -391,6 +417,8 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(address(_wrappedMToken), balance_ + accruedYield_); + unwrapAmount_ = uint240(bound(unwrapAmount_, 0, (11 * (balance_ + accruedYield_)) / 10)); + if (unwrapAmount_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); } else if (unwrapAmount_ > balance_ + accruedYield_) { @@ -402,6 +430,9 @@ contract WrappedMTokenTests is Test { unwrapAmount_ ) ); + } else { + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), unwrapAmount_); } vm.startPrank(_alice); @@ -427,7 +458,7 @@ contract WrappedMTokenTests is Test { accountEarning_ = earningEnabled_ && accountEarning_; if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); } @@ -456,6 +487,9 @@ contract WrappedMTokenTests is Test { if (balance_ + accruedYield_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); + } else { + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), balance_ + accruedYield_); } vm.startPrank(_alice); @@ -479,7 +513,7 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -487,17 +521,59 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + assertEq(_wrappedMToken.claimFor(_alice), 100); assertEq(_wrappedMToken.balanceOf(_alice), 1_100); } - function testFuzz_claimFor(uint240 balance_, uint128 accountIndex_, uint128 index_) external { + function test_claimFor_earner_withOverrideRecipient() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_bob))) + ); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + 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.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_bob), 100); + } + + function testFuzz_claimFor(uint240 balance_, uint128 accountIndex_, uint128 index_, bool claimOverride_) 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); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + if (claimOverride_) { + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_charlie))) + ); + } _wrappedMToken.enableEarning(); @@ -509,9 +585,20 @@ contract WrappedMTokenTests is Test { 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_); + } + assertEq(_wrappedMToken.claimFor(_alice), accruedYield_); - assertEq(_wrappedMToken.totalEarningSupply(), _wrappedMToken.balanceOf(_alice)); + assertEq( + _wrappedMToken.totalSupply(), + _wrappedMToken.balanceOf(_alice) + _wrappedMToken.balanceOf(_bob) + _wrappedMToken.balanceOf(_charlie) + ); } /* ============ claimExcess ============ */ @@ -539,7 +626,13 @@ contract WrappedMTokenTests is Test { uint240 expectedExcess_ = _wrappedMToken.excess(); - vm.expectCall(address(_mToken), abi.encodeCall(_mToken.transfer, (_wrappedMToken.vault(), expectedExcess_))); + vm.expectCall( + address(_mToken), + abi.encodeCall(_mToken.transfer, (_wrappedMToken.excessDestination(), expectedExcess_)) + ); + + vm.expectEmit(); + emit IWrappedMToken.ExcessClaimed(expectedExcess_); assertEq(_wrappedMToken.claimExcess(), expectedExcess_); assertEq(_wrappedMToken.excess(), 0); @@ -564,7 +657,7 @@ contract WrappedMTokenTests is Test { } function test_transfer_insufficientBalance_fromEarner_toNonEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -581,6 +674,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000); _wrappedMToken.setAccountOf(_bob, 500); + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + vm.prank(_alice); _wrappedMToken.transfer(_bob, 500); @@ -608,6 +704,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, aliceBalance_); _wrappedMToken.setAccountOf(_bob, bobBalance); + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, transferAmount_); + vm.prank(_alice); _wrappedMToken.transfer(_bob, transferAmount_); @@ -620,7 +719,7 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromEarner_toNonEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -632,6 +731,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); _wrappedMToken.setAccountOf(_bob, 500); + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + vm.prank(_alice); _wrappedMToken.transfer(_bob, 500); @@ -643,6 +745,9 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); assertEq(_wrappedMToken.totalEarningSupply(), 500); + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 1); + vm.prank(_alice); _wrappedMToken.transfer(_bob, 1); @@ -656,7 +761,7 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromNonEarner_toEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -668,6 +773,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000); _wrappedMToken.setAccountOf(_bob, 500, _currentIndex); + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + vm.prank(_alice); _wrappedMToken.transfer(_bob, 500); @@ -681,7 +789,7 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromEarner_toEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -691,6 +799,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); _wrappedMToken.setAccountOf(_bob, 500, _currentIndex); + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 500); + vm.prank(_alice); _wrappedMToken.transfer(_bob, 500); @@ -709,6 +820,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000); + vm.expectEmit(); + emit IERC20.Transfer(_alice, _alice, 500); + vm.prank(_alice); _wrappedMToken.transfer(_alice, 500); @@ -720,7 +834,7 @@ contract WrappedMTokenTests is Test { } function test_transfer_earnerToSelf() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -734,6 +848,9 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 666); + vm.expectEmit(); + emit IERC20.Transfer(_alice, _alice, 500); + vm.prank(_alice); _wrappedMToken.transfer(_alice, 500); @@ -755,7 +872,7 @@ contract WrappedMTokenTests is Test { bobEarning_ = earningEnabled_ && bobEarning_; if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); } @@ -801,7 +918,7 @@ contract WrappedMTokenTests is Test { uint240 aliceAccruedYield_ = _wrappedMToken.accruedYieldOf(_alice); uint240 bobAccruedYield_ = _wrappedMToken.accruedYieldOf(_bob); - amount_ = uint240(bound(amount_, 0, aliceBalance_ + aliceAccruedYield_)); + amount_ = uint240(bound(amount_, 0, (11 * (aliceBalance_ + aliceAccruedYield_)) / 10)); if (amount_ > aliceBalance_ + aliceAccruedYield_) { vm.expectRevert( @@ -812,6 +929,9 @@ contract WrappedMTokenTests is Test { amount_ ) ); + } else { + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, amount_); } vm.prank(_alice); @@ -842,31 +962,41 @@ contract WrappedMTokenTests is Test { } /* ============ startEarningFor ============ */ - function test_startEarningFor_notApprovedEarner() external { - vm.expectRevert(IWrappedMToken.NotApprovedEarner.selector); + function test_startEarningFor_earningIsDisabled() external { + vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); _wrappedMToken.startEarningFor(_alice); } - function test_startEarningFor_earningIsDisabled() external { - _registrar.setListContains(_EARNERS_LIST, _alice, true); + function test_startEarningFor_notApprovedEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); + _wrappedMToken.enableEarning(); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, _alice)); _wrappedMToken.startEarningFor(_alice); + } - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + function test_startEarning_overflow() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); + uint256 aliceBalance_ = uint256(type(uint112).max) + 20; - _wrappedMToken.disableEarning(); + _mToken.setCurrentIndex(_currentIndex = _EXP_SCALED_ONE); - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); + _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 { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -874,7 +1004,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000); - _registrar.setListContains(_EARNERS_LIST, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); vm.expectEmit(); emit IWrappedMToken.StartedEarning(_alice); @@ -889,30 +1019,11 @@ contract WrappedMTokenTests is Test { 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); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -920,10 +1031,13 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, balance_); - _registrar.setListContains(_EARNERS_LIST, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); _mToken.setCurrentIndex(index_); + vm.expectEmit(); + emit IWrappedMToken.StartedEarning(_alice); + _wrappedMToken.startEarningFor(_alice); assertEq(_wrappedMToken.isEarning(_alice), true); @@ -936,14 +1050,14 @@ contract WrappedMTokenTests is Test { /* ============ stopEarningFor ============ */ function test_stopEarningFor_isApprovedEarner() external { - _registrar.setListContains(_EARNERS_LIST, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - vm.expectRevert(IWrappedMToken.IsApprovedEarner.selector); + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.IsApprovedEarner.selector, _alice)); _wrappedMToken.stopEarningFor(_alice); } function test_stopEarningFor() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -969,7 +1083,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); index_ = uint128(bound(index_, accountIndex_, 10 * _EXP_SCALED_ONE)); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -981,6 +1095,9 @@ contract WrappedMTokenTests is Test { uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); + vm.expectEmit(); + emit IWrappedMToken.StoppedEarning(_alice); + _wrappedMToken.stopEarningFor(_alice); assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_); @@ -992,27 +1109,27 @@ contract WrappedMTokenTests is Test { /* ============ enableEarning ============ */ function test_enableEarning_notApprovedEarner() external { - vm.expectRevert(IWrappedMToken.NotApprovedEarner.selector); + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, address(_wrappedMToken))); _wrappedMToken.enableEarning(); } function test_enableEarning_earningCannotBeReenabled() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); _wrappedMToken.disableEarning(); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); vm.expectRevert(IWrappedMToken.EarningCannotBeReenabled.selector); _wrappedMToken.enableEarning(); } function test_enableEarning() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); vm.expectEmit(); emit IWrappedMToken.EarningEnabled(_currentIndex); @@ -1025,11 +1142,11 @@ contract WrappedMTokenTests is Test { vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); _wrappedMToken.disableEarning(); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); _wrappedMToken.disableEarning(); @@ -1038,18 +1155,18 @@ contract WrappedMTokenTests is Test { } function test_disableEarning_approvedEarner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - vm.expectRevert(IWrappedMToken.IsApprovedEarner.selector); + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.IsApprovedEarner.selector, address(_wrappedMToken))); _wrappedMToken.disableEarning(); } function test_disableEarning() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); vm.expectEmit(); emit IWrappedMToken.EarningDisabled(_currentIndex); @@ -1059,6 +1176,10 @@ contract WrappedMTokenTests is Test { /* ============ balanceOf ============ */ function test_balanceOf_nonEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + _wrappedMToken.setAccountOf(_alice, 500); assertEq(_wrappedMToken.balanceOf(_alice), 500); @@ -1066,10 +1187,14 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } function test_balanceOf_earner() external { - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -1077,15 +1202,169 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 1_000); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + _mToken.setCurrentIndex(2 * _currentIndex); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _wrappedMToken.setLastIndexOf(_alice, 2 * _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } + /* ============ balanceWithYieldOf ============ */ + function test_balanceWithYieldOf_nonEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 500); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 500); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + } + + function test_balanceWithYieldOf_earner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 550); + + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 2_200); + + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + } + + /* ============ accruedYieldOf ============ */ + function test_accruedYieldOf_nonEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 500); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + } + + function test_accruedYieldOf_earner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 50); + + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 1_200); + + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + } + + /* ============ lastIndexOf ============ */ + function test_lastIndexOf() external { + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.lastIndexOf(_alice), _EXP_SCALED_ONE); + + _wrappedMToken.setAccountOf(_alice, 0, 2 * _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.lastIndexOf(_alice), 2 * _EXP_SCALED_ONE); + } + + /* ============ isEarning ============ */ + function test_isEarning() external { + _wrappedMToken.setAccountOf(_alice, 0); + + assertFalse(_wrappedMToken.isEarning(_alice)); + + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + + assertTrue(_wrappedMToken.isEarning(_alice)); + } + + /* ============ isEarningEnabled ============ */ + function test_isEarningEnabled() external { + assertFalse(_wrappedMToken.isEarningEnabled()); + + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + assertTrue(_wrappedMToken.isEarningEnabled()); + + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); + + _wrappedMToken.disableEarning(); + + assertFalse(_wrappedMToken.isEarningEnabled()); + } + + /* ============ wasEarningEnabled ============ */ + function test_wasEarningEnabled() external { + assertFalse(_wrappedMToken.wasEarningEnabled()); + + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + assertTrue(_wrappedMToken.wasEarningEnabled()); + + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); + + _wrappedMToken.disableEarning(); + + assertTrue(_wrappedMToken.wasEarningEnabled()); + } + + /* ============ claimOverrideRecipientFor ============ */ + function test_claimOverrideRecipientFor() external { + assertEq(_wrappedMToken.claimOverrideRecipientFor(_alice), address(0)); + + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_charlie))) + ); + + assertEq(_wrappedMToken.claimOverrideRecipientFor(_alice), _charlie); + } + /* ============ totalSupply ============ */ function test_totalSupply_onlyTotalNonEarningSupply() external { _wrappedMToken.setTotalNonEarningSupply(500); @@ -1131,7 +1410,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.currentIndex(), 0); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), true); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -1141,7 +1420,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); - _registrar.setListContains(_EARNERS_LIST, address(_wrappedMToken), false); + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); _wrappedMToken.disableEarning(); @@ -1152,83 +1431,57 @@ contract WrappedMTokenTests is Test { 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); + /* ============ excess ============ */ + function test_excess() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _wrappedMToken.enableEarning(); - _mToken.setCurrentIndex(index_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE))); + assertEq(_wrappedMToken.excess(), 0); - aliceWrap_ = uint240(bound(aliceWrap_, 0, _getMaxAmount(index_) / 3)); - bobWrap_ = uint240(bound(bobWrap_, 0, _getMaxAmount(index_) / 3)); + _wrappedMToken.setTotalNonEarningSupply(1_000); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); - _mToken.setBalanceOf(_alice, aliceWrap_); - _mToken.setBalanceOf(_bob, bobWrap_); + _mToken.setBalanceOf(address(_wrappedMToken), 2_100); - if (aliceIsEarning_) { - _wrappedMToken.startEarningFor(_alice); - } + assertEq(_wrappedMToken.excess(), 0); - if (aliceWrap_ != 0) { - vm.prank(_alice); - _wrappedMToken.wrap(_alice, aliceWrap_); - } + _mToken.setBalanceOf(address(_wrappedMToken), 2_101); - _mToken.setCurrentIndex(index_ = uint128(bound(index_, index_, 10 * _EXP_SCALED_ONE))); + assertEq(_wrappedMToken.excess(), 0); - if (bobIsEarning_) { - _wrappedMToken.startEarningFor(_bob); - } + _mToken.setBalanceOf(address(_wrappedMToken), 2_102); - if (bobWrap_ != 0) { - vm.prank(_bob); - _wrappedMToken.wrap(_bob, bobWrap_); - } + assertEq(_wrappedMToken.excess(), 1); - _mToken.setCurrentIndex(index_ = uint128(bound(index_, index_, 10 * _EXP_SCALED_ONE))); + _mToken.setBalanceOf(address(_wrappedMToken), 3_102); - uint240 aliceYield_ = _wrappedMToken.accruedYieldOf(_alice); - uint240 bobYield_ = _wrappedMToken.accruedYieldOf(_bob); + assertEq(_wrappedMToken.excess(), 1_001); + } - transfer_ = uint240(bound(transfer_, 0, _wrappedMToken.balanceWithYieldOf(_alice))); + /* ============ totalAccruedYield ============ */ + function test_totalAccruedYield() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _mToken.setCurrentIndex(index_ = uint128(bound(index_, index_, 10 * _EXP_SCALED_ONE))); + _wrappedMToken.enableEarning(); - aliceYield_ += _wrappedMToken.accruedYieldOf(_alice); + _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setTotalEarningSupply(1_000); - if (_wrappedMToken.balanceWithYieldOf(_alice) != 0) { - vm.prank(_alice); - _wrappedMToken.unwrap(_charlie); - } + assertEq(_wrappedMToken.totalAccruedYield(), 0); - _mToken.setCurrentIndex(index_ = uint128(bound(index_, index_, 10 * _EXP_SCALED_ONE))); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); - bobYield_ += _wrappedMToken.accruedYieldOf(_bob); + assertEq(_wrappedMToken.totalAccruedYield(), 100); - if (_wrappedMToken.balanceWithYieldOf(_bob) != 0) { - vm.prank(_bob); - _wrappedMToken.unwrap(_charlie); - } + _wrappedMToken.setTotalEarningSupply(900); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 200); - uint240 total_ = aliceWrap_ + aliceYield_ + bobWrap_ + bobYield_; + _mToken.setCurrentIndex(_currentIndex = 1_210000000000); - if (total_ < 100e6) { - assertApproxEqAbs(_mToken.balanceOf(_charlie), total_, 100); - } else { - assertApproxEqRel(_mToken.balanceOf(_charlie), total_, 1e12); - } + assertEq(_wrappedMToken.totalAccruedYield(), 310); } /* ============ utils ============ */ diff --git a/test/utils/Invariants.sol b/test/utils/Invariants.sol index 086cc1f..9f1eff6 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 { diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index 56968c1..805eb7b 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -1,10 +1,8 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; contract MockM { - address public ttgRegistrar; - uint128 public currentIndex; mapping(address account => uint256 balance) public balanceOf; @@ -32,10 +30,6 @@ contract MockM { currentIndex = currentIndex_; } - function setTtgRegistrar(address ttgRegistrar_) external { - ttgRegistrar = ttgRegistrar_; - } - function startEarning() external { isEarning[msg.sender] = true; } @@ -46,8 +40,6 @@ contract MockM { } contract MockRegistrar { - address public vault; - mapping(bytes32 key => bytes32 value) public get; mapping(bytes32 list => mapping(address account => bool contains)) public listContains; @@ -59,8 +51,4 @@ contract MockRegistrar { function setListContains(bytes32 list_, address account_, bool contains_) external { listContains[list_][account_] = contains_; } - - function setVault(address vault_) external { - vault = vault_; - } } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index 9bc9868..b285c83 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -1,11 +1,16 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { WrappedMToken } from "../../src/WrappedMToken.sol"; contract WrappedMTokenHarness is WrappedMToken { - constructor(address mToken_, address migrationAdmin_) WrappedMToken(mToken_, migrationAdmin_) {} + constructor( + address mToken_, + address registrar_, + address excessDestination_, + address migrationAdmin_ + ) WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_) {} function setIsEarningOf(address account_, bool isEarning_) external { _accounts[account_].isEarning = isEarning_; From d0f6c8c296aa68f1f6d57c65f8fa8d8a64894ada Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:03:30 -0500 Subject: [PATCH 02/27] feat: `wrapWithPermit` (#98) --- src/WrappedMToken.sol | 26 +++ src/interfaces/IMTokenLike.sol | 30 ++++ src/interfaces/IWrappedMToken.sol | 34 ++++ test/integration/Protocol.t.sol | 16 ++ test/integration/TestBase.sol | 67 ++++++++ test/unit/WrappedMToken.t.sol | 255 +++++++++++++++++++++++----- test/utils/Mocks.sol | 18 ++ test/utils/WrappedMTokenHarness.sol | 12 ++ 8 files changed, 412 insertions(+), 46 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 096231c..e9df011 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -121,6 +121,32 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return _wrap(msg.sender, recipient_, _mBalanceOf(msg.sender)); } + /// @inheritdoc IWrappedMToken + function wrapWithPermit( + address recipient_, + uint256 amount_, + uint256 deadline_, + uint8 v_, + bytes32 r_, + bytes32 s_ + ) external returns (uint240 wrapped_) { + IMTokenLike(mToken).permit(msg.sender, address(this), amount_, deadline_, v_, r_, s_); + + return _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); + } + + /// @inheritdoc IWrappedMToken + function wrapWithPermit( + address recipient_, + uint256 amount_, + uint256 deadline_, + bytes memory signature_ + ) external returns (uint240 wrapped_) { + IMTokenLike(mToken).permit(msg.sender, address(this), amount_, deadline_, signature_); + + return _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); + } + /// @inheritdoc IWrappedMToken function unwrap(address recipient_, uint256 amount_) external returns (uint240 unwrapped_) { return _unwrap(msg.sender, recipient_, UIntMath.safe240(amount_)); diff --git a/src/interfaces/IMTokenLike.sol b/src/interfaces/IMTokenLike.sol index 3d902bc..15fb597 100644 --- a/src/interfaces/IMTokenLike.sol +++ b/src/interfaces/IMTokenLike.sol @@ -9,6 +9,36 @@ pragma solidity 0.8.26; 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. diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index a74ca47..3fc3cc3 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -113,6 +113,40 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ function wrap(address recipient) external returns (uint240 wrapped); + /** + * @notice Wraps `amount` M from the caller into wM for `recipient`, using a permit. + * @param recipient The account receiving the minted wM. + * @param amount The amount of M deposited. + * @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). + * @return wrapped The amount of wM minted. + */ + function wrapWithPermit( + address recipient, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint240 wrapped); + + /** + * @notice Wraps `amount` M from the caller into wM for `recipient`, using a permit. + * @param recipient The account receiving the minted wM. + * @param amount The amount of M deposited. + * @param deadline The last timestamp where the signature is still valid. + * @param signature An arbitrary signature (EIP-712). + * @return wrapped The amount of wM minted. + */ + function wrapWithPermit( + address recipient, + uint256 amount, + uint256 deadline, + bytes memory signature + ) external returns (uint240 wrapped); + /** * @notice Unwraps `amount` wM from the caller into M for `recipient`. * @param recipient The account receiving the withdrawn M. diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index dffe6c6..f6688cd 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -63,6 +63,22 @@ contract ProtocolIntegrationTests is TestBase { assertTrue(_mToken.isEarning(address(_wrappedMToken))); } + function test_wrapWithPermits() external { + _giveM(_alice, 200_000000); + + assertEq(_mToken.balanceOf(_alice), 200_000000); + + _wrapWithPermitVRS(_alice, _aliceKey, _alice, 100_000000, 0, block.timestamp); + + assertEq(_mToken.balanceOf(_alice), 100_000000); + assertEq(_wrappedMToken.balanceOf(_alice), 99_999999); + + _wrapWithPermitSignature(_alice, _aliceKey, _alice, 100_000000, 1, block.timestamp); + + assertEq(_mToken.balanceOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 199_999999); + } + function test_integration_yieldAccumulation() external { _giveM(_alice, 100_000000); diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index f51601c..523d731 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.26; 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 { Test } from "../../lib/forge-std/src/Test.sol"; @@ -55,6 +57,8 @@ 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 _implementationV2; @@ -113,6 +117,34 @@ contract TestBase is Test { _wrappedMToken.wrap(recipient_); } + function _wrapWithPermitVRS( + address account_, + uint256 signerPrivateKey_, + address recipient_, + uint256 amount_, + uint256 nonce_, + uint256 deadline_ + ) internal { + (uint8 v_, bytes32 r_, bytes32 s_) = _getPermit(account_, signerPrivateKey_, amount_, nonce_, deadline_); + + vm.prank(account_); + _wrappedMToken.wrapWithPermit(recipient_, amount_, deadline_, v_, r_, s_); + } + + function _wrapWithPermitSignature( + address account_, + uint256 signerPrivateKey_, + address recipient_, + uint256 amount_, + uint256 nonce_, + uint256 deadline_ + ) internal { + (uint8 v_, bytes32 r_, bytes32 s_) = _getPermit(account_, signerPrivateKey_, amount_, nonce_, deadline_); + + vm.prank(account_); + _wrappedMToken.wrapWithPermit(recipient_, amount_, deadline_, abi.encodePacked(r_, s_, v_)); + } + function _unwrap(address account_, address recipient_, uint256 amount_) internal { vm.prank(account_); _wrappedMToken.unwrap(recipient_, amount_); @@ -162,4 +194,39 @@ contract TestBase is Test { vm.prank(_migrationAdmin); _wrappedMToken.migrate(_migratorV1); } + + /* ============ 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/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 53823ab..169d974 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -107,36 +107,28 @@ contract WrappedMTokenTests is Test { WrappedMTokenHarness(address(new Proxy(address(0)))); } - /* ============ wrap ============ */ - function test_wrap_insufficientAmount() external { + /* ============ _wrap ============ */ + function test_internalWrap_insufficientAmount() external { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); - _wrappedMToken.wrap(_alice, 0); + _wrappedMToken.internalWrap(_alice, _alice, 0); } - function test_wrap_invalidRecipient() external { + function test_internalWrap_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); + _wrappedMToken.internalWrap(_alice, address(0), 1_000); } - function test_wrap_toNonEarner() external { + function test_internalWrap_toNonEarner() external { _mToken.setBalanceOf(_alice, 1_000); - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 1_000), 1_000); + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 1_000); + + assertEq(_wrappedMToken.internalWrap(_alice, _alice, 1_000), 1_000); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); @@ -144,7 +136,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); } - function test_wrap_toEarner() external { + function test_internalWrap_toEarner() external { _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -153,8 +145,10 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(_alice, 1_002); - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 999), 999); + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 999); + + assertEq(_wrappedMToken.internalWrap(_alice, _alice, 999), 999); assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); assertEq(_wrappedMToken.balanceOf(_alice), 999); @@ -162,8 +156,10 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 909); assertEq(_wrappedMToken.totalEarningSupply(), 999); - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 1), 1); + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 1); + + assertEq(_wrappedMToken.internalWrap(_alice, _alice, 1), 1); // No change due to principal round down on wrap. assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); @@ -172,8 +168,10 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 910); assertEq(_wrappedMToken.totalEarningSupply(), 1_000); - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 2), 2); + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 2); + + assertEq(_wrappedMToken.internalWrap(_alice, _alice, 2), 2); assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); assertEq(_wrappedMToken.balanceOf(_alice), 1_002); @@ -182,6 +180,14 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 1_002); } + /* ============ wrap ============ */ + function test_wrap_invalidAmount() external { + vm.expectRevert(UIntMath.InvalidUInt240.selector); + + vm.prank(_alice); + _wrappedMToken.wrap(_alice, uint256(type(uint240).max) + 1); + } + function testFuzz_wrap( bool earningEnabled_, bool accountEarning_, @@ -240,7 +246,17 @@ contract WrappedMTokenTests is Test { ); } - function testFuzz_wrapFull( + /* ============ wrap entire balance ============ */ + function test_wrap_entireBalance_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 testFuzz_wrap_entireBalance( bool earningEnabled_, bool accountEarning_, uint240 balance_, @@ -298,22 +314,153 @@ contract WrappedMTokenTests is Test { ); } - /* ============ unwrap ============ */ - function test_unwrap_insufficientAmount() external { + /* ============ wrapWithPermit vrs ============ */ + function test_wrapWithPermit_vrs_invalidAmount() external { + vm.expectRevert(UIntMath.InvalidUInt240.selector); + + vm.prank(_alice); + _wrappedMToken.wrapWithPermit(_alice, uint256(type(uint240).max) + 1, 0, 0, bytes32(0), bytes32(0)); + } + + function testFuzz_wrapWithPermit_vrs( + bool earningEnabled_, + bool accountEarning_, + uint240 balance_, + uint240 wrapAmount_, + uint128 accountIndex_, + uint128 currentIndex_ + ) external { + accountEarning_ = earningEnabled_ && accountEarning_; + + if (earningEnabled_) { + _registrar.setListContains(_EARNERS_LIST_NAME, 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))); + } else { + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, wrapAmount_); + } + + vm.startPrank(_alice); + _wrappedMToken.wrapWithPermit(_alice, wrapAmount_, 0, 0, bytes32(0), bytes32(0)); + + if (wrapAmount_ == 0) return; + + assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ + wrapAmount_); + + assertEq( + accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), + _wrappedMToken.balanceOf(_alice) + ); + } + + /* ============ wrapWithPermit signature ============ */ + function test_wrapWithPermit_signature_invalidAmount() external { + vm.expectRevert(UIntMath.InvalidUInt240.selector); + + vm.prank(_alice); + _wrappedMToken.wrapWithPermit(_alice, uint256(type(uint240).max) + 1, 0, hex""); + } + + function testFuzz_wrapWithPermit_signature( + bool earningEnabled_, + bool accountEarning_, + uint240 balance_, + uint240 wrapAmount_, + uint128 accountIndex_, + uint128 currentIndex_ + ) external { + accountEarning_ = earningEnabled_ && accountEarning_; + + if (earningEnabled_) { + _registrar.setListContains(_EARNERS_LIST_NAME, 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))); + } else { + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, wrapAmount_); + } + + vm.startPrank(_alice); + _wrappedMToken.wrapWithPermit(_alice, wrapAmount_, 0, hex""); + + if (wrapAmount_ == 0) return; + + assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ + wrapAmount_); + + assertEq( + accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), + _wrappedMToken.balanceOf(_alice) + ); + } + + /* ============ _unwrap ============ */ + function test_internalUnwrap_insufficientAmount() external { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); - _wrappedMToken.unwrap(_alice, 0); + _wrappedMToken.internalUnwrap(_alice, _alice, 0); } - function test_unwrap_insufficientBalance_fromNonEarner() external { + 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.unwrap(_alice, 1_000); + _wrappedMToken.internalUnwrap(_alice, _alice, 1_000); } - function test_unwrap_insufficientBalance_fromEarner() external { + function test_internalUnwrap_insufficientBalance_fromEarner() external { _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -321,27 +468,30 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); - vm.prank(_alice); - _wrappedMToken.unwrap(_alice, 1_000); + _wrappedMToken.internalUnwrap(_alice, _alice, 1_000); } - function test_unwrap_fromNonEarner() external { + function test_internalUnwrap_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); + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 500); + + assertEq(_wrappedMToken.internalUnwrap(_alice, _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); + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 500); + + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 500); assertEq(_wrappedMToken.balanceOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); @@ -349,7 +499,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); } - function test_unwrap_fromEarner() external { + function test_internalUnwrap_fromEarner() external { _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -361,8 +511,10 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(address(_wrappedMToken), 1_000); - vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 1), 0); + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 1); + + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 0); // Change due to principal round up on unwrap. assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); @@ -370,8 +522,10 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningSupply(), 999); - vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 999), 998); + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 999); + + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 999), 998); assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); assertEq(_wrappedMToken.balanceOf(_alice), 0); @@ -379,6 +533,14 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 0); } + /* ============ unwrap ============ */ + function test_unwrap_invalidAmount() external { + vm.expectRevert(UIntMath.InvalidUInt240.selector); + + vm.prank(_alice); + _wrappedMToken.unwrap(_alice, uint256(type(uint240).max) + 1); + } + function testFuzz_unwrap( bool earningEnabled_, bool accountEarning_, @@ -448,7 +610,8 @@ contract WrappedMTokenTests is Test { ); } - function testFuzz_unwrapFull( + /* ============ unwrap entire balance ============ */ + function testFuzz_unwrap_entireBalance( bool earningEnabled_, bool accountEarning_, uint240 balance_, diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index 805eb7b..0f1bf8c 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -8,6 +8,24 @@ contract MockM { mapping(address account => uint256 balance) public balanceOf; mapping(address account => bool isEarning) public isEarning; + 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_; balanceOf[recipient_] += amount_; diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index b285c83..6d840f3 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -12,6 +12,18 @@ contract WrappedMTokenHarness is WrappedMToken { address migrationAdmin_ ) WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_) {} + function internalWrap(address account_, address recipient_, uint240 amount_) external returns (uint240 wrapped_) { + return _wrap(account_, recipient_, amount_); + } + + function internalUnwrap( + address account_, + address recipient_, + uint240 amount_ + ) external returns (uint240 unwrapped_) { + return _unwrap(account_, recipient_, amount_); + } + function setIsEarningOf(address account_, bool isEarning_) external { _accounts[account_].isEarning = isEarning_; } From 86abe79c479c156174c1ced20cef7a3294ef093b Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:10:11 -0500 Subject: [PATCH 03/27] feat: `setClaimRecipient` (#99) --- src/WrappedMToken.sol | 35 +++++--- src/interfaces/IWrappedMToken.sol | 15 +++- test/integration/UniswapV3.t.sol | 2 +- test/unit/WrappedMToken.t.sol | 129 ++++++++++++++++++++-------- test/utils/WrappedMTokenHarness.sol | 20 +++-- 5 files changed, 144 insertions(+), 57 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index e9df011..d9f4d33 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -34,9 +34,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /** * @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 lastIndex The index of the last interaction for the account (0 for non-earning accounts). + * @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). + * @param hasClaimRecipient Whether the account has an explicitly set claim recipient. */ struct Account { // First Slot @@ -44,6 +45,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { uint240 balance; // Second slot uint128 lastIndex; + bool hasClaimRecipient; } /* ============ Variables ============ */ @@ -87,6 +89,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @dev Array of indices at which earning was enabled or disabled. uint128[] internal _enableDisableEarningIndices; + mapping(address account => address claimRecipient) internal _claimRecipients; + /* ============ Constructor ============ */ /** @@ -216,6 +220,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { _stopEarningFor(account_, currentIndex()); } + /// @inheritdoc IWrappedMToken + function setClaimRecipient(address claimRecipient_) external { + _accounts[msg.sender].hasClaimRecipient = (_claimRecipients[msg.sender] = claimRecipient_) != address(0); + + emit ClaimRecipientSet(msg.sender, claimRecipient_); + } + /* ============ Temporary Admin Migration ============ */ /// @inheritdoc IWrappedMToken @@ -253,13 +264,14 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /// @inheritdoc IWrappedMToken - function claimOverrideRecipientFor(address account_) public view returns (address recipient_) { - return - address( - uint160( - uint256(_getFromRegistrar(keccak256(abi.encode(CLAIM_OVERRIDE_RECIPIENT_KEY_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 @@ -454,8 +466,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { totalEarningSupply += yield_; } - address claimOverrideRecipient_ = claimOverrideRecipientFor(account_); - address claimRecipient_ = claimOverrideRecipient_ == address(0) ? account_ : claimOverrideRecipient_; + address claimRecipient_ = claimRecipientFor(account_); // Emit the appropriate `Claimed` and `Transfer` events, depending on the claim override recipient emit Claimed(account_, claimRecipient_, yield_); diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 3fc3cc3..22f672f 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -20,6 +20,13 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ event Claimed(address indexed account, address indexed recipient, uint240 yield); + /** + * @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 index at the moment earning is enabled. @@ -193,6 +200,12 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ function stopEarningFor(address account) 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; + /* ============ Temporary Admin Migration ============ */ /** @@ -241,7 +254,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { * @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 Wrapped M's earning mechanism. function currentIndex() external view returns (uint128 index); diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index e06721d..eb79457 100644 --- a/test/integration/UniswapV3.t.sol +++ b/test/integration/UniswapV3.t.sol @@ -56,7 +56,7 @@ contract UniswapV3IntegrationTests is TestBase { _deployV2Components(); _migrate(); - _poolClaimRecipient = _wrappedMToken.claimOverrideRecipientFor(_pool); + _poolClaimRecipient = _wrappedMToken.claimRecipientFor(_pool); _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); _poolBalanceOfUSDC = IERC20(_USDC).balanceOf(_pool); diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 169d974..c456131 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -141,7 +141,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); _mToken.setBalanceOf(_alice, 1_002); @@ -207,7 +207,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -275,7 +275,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -341,7 +341,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -407,7 +407,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -465,7 +465,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); _wrappedMToken.internalUnwrap(_alice, _alice, 1_000); @@ -507,7 +507,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setPrincipalOfTotalEarningSupply(909); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); _mToken.setBalanceOf(address(_wrappedMToken), 1_000); @@ -560,7 +560,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -629,7 +629,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -680,7 +680,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -705,7 +705,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -742,7 +742,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningSupply(balance_); - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); _mToken.setCurrentIndex(index_); @@ -824,7 +824,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); vm.prank(_alice); @@ -891,7 +891,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalNonEarningSupply(500); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); _wrappedMToken.setAccountOf(_bob, 500); vm.expectEmit(); @@ -934,7 +934,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalNonEarningSupply(1_000); _wrappedMToken.setAccountOf(_alice, 1_000); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex); + _wrappedMToken.setAccountOf(_bob, 500, _currentIndex, false); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -959,8 +959,8 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setPrincipalOfTotalEarningSupply(1_363); _wrappedMToken.setTotalEarningSupply(1_500); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); + _wrappedMToken.setAccountOf(_bob, 500, _currentIndex, false); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -1004,7 +1004,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setPrincipalOfTotalEarningSupply(909); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); _mToken.setCurrentIndex((_currentIndex * 5) / 3); // 1_833333447838 @@ -1043,7 +1043,7 @@ contract WrappedMTokenTests is Test { aliceBalance_ = uint240(bound(aliceBalance_, 0, _getMaxAmount(aliceIndex_) / 4)); if (aliceEarning_) { - _wrappedMToken.setAccountOf(_alice, aliceBalance_, aliceIndex_); + _wrappedMToken.setAccountOf(_alice, aliceBalance_, aliceIndex_, false); _wrappedMToken.setTotalEarningSupply(aliceBalance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -1058,7 +1058,7 @@ contract WrappedMTokenTests is Test { bobBalance_ = uint240(bound(bobBalance_, 0, _getMaxAmount(bobIndex_) / 4)); if (bobEarning_) { - _wrappedMToken.setAccountOf(_bob, bobBalance_, bobIndex_); + _wrappedMToken.setAccountOf(_bob, bobBalance_, bobIndex_, false); _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + bobBalance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -1227,7 +1227,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setPrincipalOfTotalEarningSupply(909); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); vm.expectEmit(); emit IWrappedMToken.StoppedEarning(_alice); @@ -1252,7 +1252,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningSupply(balance_); - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); _mToken.setCurrentIndex(index_); @@ -1270,6 +1270,38 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 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)); + } + /* ============ enableEarning ============ */ function test_enableEarning_notApprovedEarner() external { vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, address(_wrappedMToken))); @@ -1361,11 +1393,11 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -1373,7 +1405,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } @@ -1402,11 +1434,11 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 550); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); @@ -1414,7 +1446,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 2_200); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); } @@ -1443,11 +1475,11 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.accruedYieldOf(_alice), 50); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -1455,18 +1487,18 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 1_200); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); } /* ============ lastIndexOf ============ */ function test_lastIndexOf() external { - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.lastIndexOf(_alice), _EXP_SCALED_ONE); - _wrappedMToken.setAccountOf(_alice, 0, 2 * _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 0, 2 * _EXP_SCALED_ONE, false); assertEq(_wrappedMToken.lastIndexOf(_alice), 2 * _EXP_SCALED_ONE); } @@ -1477,7 +1509,7 @@ contract WrappedMTokenTests is Test { assertFalse(_wrappedMToken.isEarning(_alice)); - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); assertTrue(_wrappedMToken.isEarning(_alice)); } @@ -1516,16 +1548,37 @@ contract WrappedMTokenTests is Test { assertTrue(_wrappedMToken.wasEarningEnabled()); } - /* ============ claimOverrideRecipientFor ============ */ - function test_claimOverrideRecipientFor() external { - assertEq(_wrappedMToken.claimOverrideRecipientFor(_alice), address(0)); + /* ============ 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.claimOverrideRecipientFor(_alice), _charlie); + assertEq(_wrappedMToken.claimRecipientFor(_alice), _bob); } /* ============ totalSupply ============ */ diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index 6d840f3..a85b766 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -32,12 +32,16 @@ contract WrappedMTokenHarness is WrappedMToken { _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_, uint256 index_, bool hasClaimRecipient_) external { + _accounts[account_] = Account(true, uint240(balance_), uint128(index_), hasClaimRecipient_); } function setAccountOf(address account_, uint256 balance_) external { - _accounts[account_] = Account(false, uint240(balance_), 0); + _accounts[account_] = Account(false, uint240(balance_), 0, false); + } + + function setInternalClaimRecipient(address account_, address claimRecipient_) external { + _claimRecipients[account_] = claimRecipient_; } function setTotalNonEarningSupply(uint256 totalNonEarningSupply_) external { @@ -52,8 +56,14 @@ contract WrappedMTokenHarness is WrappedMToken { principalOfTotalEarningSupply = uint112(principalOfTotalEarningSupply_); } - function getAccountOf(address account_) external view returns (bool isEarning_, uint240 balance_, uint128 index_) { + function getAccountOf( + address account_ + ) external view returns (bool isEarning_, uint240 balance_, uint128 index_, bool hasClaimRecipient_) { Account storage account = _accounts[account_]; - return (account.isEarning, account.balance, account.lastIndex); + return (account.isEarning, account.balance, account.lastIndex, account.hasClaimRecipient); + } + + function getInternalClaimRecipientOf(address account_) external view returns (address claimRecipient_) { + return _claimRecipients[account_]; } } From 442817b057c564dc95713ae597430132cc8889d4 Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:37:02 -0500 Subject: [PATCH 04/27] feat: principal-based account (#108) - principal-based earner account, instead of last index - migration for existing earners on upgrade --- .github/workflows/coverage.yml | 2 +- .github/workflows/test-gas.yml | 2 +- Makefile | 4 +- lib/common | 2 +- script/DeployBase.sol | 5 +- script/DeployUpgradeMainnet.s.sol | 53 +- src/ListOfEarnersToMigrate.sol | 19 + src/MigratorV1.sol | 77 +- src/WrappedMToken.sol | 152 ++-- src/interfaces/IWrappedMToken.sol | 18 +- test.sh | 2 +- test/integration/MorphoBlue.t.sol | 6 +- test/integration/Protocol.t.sol | 48 +- test/integration/TestBase.sol | 47 +- test/integration/UniswapV3.t.sol | 2 +- test/integration/Upgrade.t.sol | 47 +- test/unit/Migration.t.sol | 4 +- test/unit/Stories.t.sol | 32 +- test/unit/WrappedMToken.t.sol | 1089 +++++++++++++++------------ test/utils/Invariants.sol | 15 +- test/utils/Mocks.sol | 4 + test/utils/WrappedMTokenHarness.sol | 25 +- 22 files changed, 1017 insertions(+), 638 deletions(-) create mode 100644 src/ListOfEarnersToMigrate.sol 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/Makefile b/Makefile index 61be6b6..805bc44 100644 --- a/Makefile +++ b/Makefile @@ -38,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/lib/common b/lib/common index 3692db1..5f5719e 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 3692db150ad90b21d7c213ea535f34792ad8873f +Subproject commit 5f5719e4764288957f3adf6884ae7dc83d9a7fda diff --git a/script/DeployBase.sol b/script/DeployBase.sol index c4e1325..a6a6411 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -44,13 +44,14 @@ contract DeployBase { address mToken_, address registrar_, address excessDestination_, - address migrationAdmin_ + address migrationAdmin_, + address[] memory earners_ ) public virtual returns (address implementation_, address migrator_) { // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. // Migrator needs `implementation_` addresses. implementation_ = address(new WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_)); - migrator_ = address(new MigratorV1(implementation_)); + migrator_ = address(new MigratorV1(implementation_, earners_)); } /** diff --git a/script/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol index 060e563..ad0fd17 100644 --- a/script/DeployUpgradeMainnet.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -38,6 +38,45 @@ contract DeployUpgradeMainnet is Script, DeployBase { // NOTE: Ensure this is the correct expected mainnet address for the Migrator. address internal constant _EXPECTED_MIGRATOR = address(0); + // NOTE: Ensure this is the correct and complete list of earners on mainnet. + 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 run() external { address deployer_ = vm.rememberKey(vm.envUint("PRIVATE_KEY")); @@ -73,7 +112,19 @@ contract DeployUpgradeMainnet is Script, DeployBase { if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); - (implementation_, migrator_) = deployUpgrade(_M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _MIGRATION_ADMIN); + address[] memory earners_ = new address[](_earners.length); + + for (uint256 index_; index_ < _earners.length; ++index_) { + earners_[index_] = _earners[index_]; + } + + (implementation_, migrator_) = deployUpgrade( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _MIGRATION_ADMIN, + earners_ + ); vm.stopBroadcast(); diff --git a/src/ListOfEarnersToMigrate.sol b/src/ListOfEarnersToMigrate.sol new file mode 100644 index 0000000..4c0b8ea --- /dev/null +++ b/src/ListOfEarnersToMigrate.sol @@ -0,0 +1,19 @@ +// 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 M^0 Labs + */ +contract ListOfEarnersToMigrate { + address[] public earners; + + constructor(address[] memory earners_) { + earners = earners_; + } + + function getEarners() external view returns (address[] memory earners_) { + return earners; + } +} diff --git a/src/MigratorV1.sol b/src/MigratorV1.sol index 74cef78..8eaef35 100644 --- a/src/MigratorV1.sol +++ b/src/MigratorV1.sol @@ -2,25 +2,100 @@ 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 M^0 Labs */ contract MigratorV1 { + /* ============ 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; - constructor(address newImplementation_) { + address public immutable listOfEarnerToMigrate; + + constructor(address newImplementation_, address[] memory earners_) { newImplementation = newImplementation_; + + listOfEarnerToMigrate = address(new ListOfEarnersToMigrate(earners_)); } fallback() external virtual { + _migrateEarners(); + address newImplementation_ = newImplementation; assembly { sstore(_IMPLEMENTATION_SLOT, newImplementation_) } } + + 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. + } + } } diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index d9f4d33..56970b3 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -33,10 +33,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /* ============ Structs ============ */ /** - * @dev Struct to represent an account's balance and yield earning details + * @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 lastIndex The index of the last interaction for the account (0 for non-earning accounts). + * @param earningPrincipal The earning principal for the account. * @param hasClaimRecipient Whether the account has an explicitly set claim recipient. */ struct Account { @@ -44,7 +44,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { bool isEarning; uint240 balance; // Second slot - uint128 lastIndex; + uint112 earningPrincipal; bool hasClaimRecipient; } @@ -75,7 +75,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { address public immutable excessDestination; /// @inheritdoc IWrappedMToken - uint112 public principalOfTotalEarningSupply; + uint112 public totalEarningPrincipal; /// @inheritdoc IWrappedMToken uint240 public totalEarningSupply; @@ -243,7 +243,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { Account storage accountInfo_ = _accounts[account_]; return - accountInfo_.isEarning ? _getAccruedYield(accountInfo_.balance, accountInfo_.lastIndex, currentIndex()) : 0; + accountInfo_.isEarning + ? _getAccruedYield(accountInfo_.balance, accountInfo_.earningPrincipal, currentIndex()) + : 0; } /// @inheritdoc IERC20 @@ -259,8 +261,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /// @inheritdoc IWrappedMToken - function lastIndexOf(address account_) external view returns (uint128 lastIndex_) { - return _accounts[account_].lastIndex; + function earningPrincipalOf(address account_) external view returns (uint112 earningPrincipal_) { + return _accounts[account_].earningPrincipal; } /// @inheritdoc IWrappedMToken @@ -276,7 +278,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function currentIndex() public view returns (uint128 index_) { - return isEarningEnabled() ? _currentMIndex() : _lastDisableEarningIndex(); + if (isEarningEnabled()) return _currentMIndex(); + + return UIntMath.max128(IndexingMath.EXP_SCALED_ONE, _lastDisableEarningIndex()); } /// @inheritdoc IWrappedMToken @@ -390,7 +394,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { */ function _subtractNonEarningAmount(address account_, uint240 amount_) internal { Account storage accountInfo_ = _accounts[account_]; - uint240 balance_ = accountInfo_.balance; if (balance_ < amount_) revert InsufficientBalance(account_, balance_, amount_); @@ -402,22 +405,26 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /** - * @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_]; + uint112 principal_ = 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_; + accountInfo_.balance += amount_; + accountInfo_.earningPrincipal = UIntMath.safe112(uint256(accountInfo_.earningPrincipal) + principal_); } - _addTotalEarningSupply(amount_, currentIndex_); + _addTotalEarningSupply(amount_, principal_); } /** - * @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. @@ -429,11 +436,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (balance_ < amount_) revert InsufficientBalance(account_, balance_, amount_); + uint112 earningPrincipal_ = accountInfo_.earningPrincipal; + + uint112 principal_ = UIntMath.min112( + IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_), + earningPrincipal_ + ); + unchecked { accountInfo_.balance = balance_ - amount_; + accountInfo_.earningPrincipal = earningPrincipal_ - principal_; } - _subtractTotalEarningSupply(amount_, currentIndex_); + _subtractTotalEarningSupply(amount_, principal_); } /** @@ -447,22 +462,15 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (!accountInfo_.isEarning) return 0; - uint128 index_ = accountInfo_.lastIndex; - - if (currentIndex_ == index_) return 0; - uint240 startingBalance_ = accountInfo_.balance; - yield_ = _getAccruedYield(startingBalance_, index_, currentIndex_); - - accountInfo_.lastIndex = currentIndex_; + yield_ = _getAccruedYield(startingBalance_, accountInfo_.earningPrincipal, currentIndex_); if (yield_ == 0) return 0; unchecked { + // 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_; } @@ -488,8 +496,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function _transfer(address sender_, address recipient_, uint240 amount_, uint128 currentIndex_) internal { _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_); @@ -497,29 +503,21 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (amount_ == 0) return; - Account storage senderAccountInfo_ = _accounts[sender_]; - Account storage recipientAccountInfo_ = _accounts[recipient_]; + if (sender_ == recipient_) { + uint240 balance_ = _accounts[sender_].balance; - // 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; - - if (senderBalance_ < amount_) revert InsufficientBalance(sender_, senderBalance_, amount_); - - unchecked { - senderAccountInfo_.balance = senderBalance_ - amount_; - recipientAccountInfo_.balance += amount_; - } + if (balance_ < amount_) revert InsufficientBalance(sender_, balance_, amount_); return; } - senderAccountInfo_.isEarning + // TODO: Don't touch globals if both are earning or non-earning. + + _accounts[sender_].isEarning ? _subtractEarningAmount(sender_, amount_, currentIndex_) : _subtractNonEarningAmount(sender_, amount_); - recipientAccountInfo_.isEarning + _accounts[recipient_].isEarning ? _addEarningAmount(recipient_, amount_, currentIndex_) : _addNonEarningAmount(recipient_, amount_); } @@ -536,38 +534,29 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /** * @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) { - delete totalEarningSupply; - delete principalOfTotalEarningSupply; - - return; - } - - uint112 principal_ = IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_); + function _subtractTotalEarningSupply(uint240 amount_, uint112 principal_) internal { + uint240 totalEarningSupply_ = totalEarningSupply; + uint112 totalEarningPrincipal_ = totalEarningPrincipal; unchecked { - principalOfTotalEarningSupply -= ( - principal_ > principalOfTotalEarningSupply ? principalOfTotalEarningSupply : principal_ - ); - - totalEarningSupply -= amount_; + totalEarningSupply = totalEarningSupply_ - UIntMath.min240(amount_, totalEarningSupply_); + totalEarningPrincipal = totalEarningPrincipal_ - UIntMath.min112(principal_, totalEarningPrincipal_); } } @@ -625,12 +614,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (accountInfo_.isEarning) return; - accountInfo_.isEarning = true; - accountInfo_.lastIndex = currentIndex_; - uint240 balance_ = accountInfo_.balance; + uint112 earningPrincipal_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); - _addTotalEarningSupply(balance_, currentIndex_); + accountInfo_.isEarning = true; + accountInfo_.earningPrincipal = earningPrincipal_; + + _addTotalEarningSupply(balance_, earningPrincipal_); unchecked { totalNonEarningSupply -= balance_; @@ -647,18 +637,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function _stopEarningFor(address account_, uint128 currentIndex_) internal { _revertIfApprovedEarner(account_); - _claim(account_, currentIndex_); - Account storage accountInfo_ = _accounts[account_]; if (!accountInfo_.isEarning) return; - delete accountInfo_.isEarning; - delete accountInfo_.lastIndex; + _claim(account_, currentIndex_); uint240 balance_ = accountInfo_.balance; + uint112 earningPrincipal_ = accountInfo_.earningPrincipal; + + delete accountInfo_.isEarning; + delete accountInfo_.earningPrincipal; - _subtractTotalEarningSupply(balance_, currentIndex_); + _subtractTotalEarningSupply(balance_, earningPrincipal_); unchecked { totalNonEarningSupply += balance_; @@ -680,21 +671,18 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /** - * @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_; @@ -765,7 +753,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @return supply_ The projected total earning supply. */ function _projectedEarningSupply(uint128 currentIndex_) internal view returns (uint240 supply_) { - return IndexingMath.getPresentAmountRoundedDown(principalOfTotalEarningSupply, currentIndex_); + return + UIntMath.max240( + IndexingMath.getPresentAmountRoundedDown(totalEarningPrincipal, currentIndex_), + totalEarningSupply + ); } /** diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 22f672f..ef991ae 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -69,7 +69,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { error EarningCannotBeReenabled(); /** - * @notice Emitted when calling `stopEarning` for an account approved as earner by the Registrar. + * @notice Emitted when calling `stopEarning` for an account approved as an earner by the Registrar. * @param account The account that is an approved earner. */ error IsApprovedEarner(address account); @@ -83,7 +83,7 @@ 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 the Registrar. + * @notice Emitted when calling `startEarning` for an account not approved as an earner by the Registrar. * @param account The account that is not an approved earner. */ error NotApprovedEarner(address account); @@ -243,11 +243,11 @@ 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. @@ -265,7 +265,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /** * @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); @@ -293,8 +293,8 @@ 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); 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/integration/MorphoBlue.t.sol b/test/integration/MorphoBlue.t.sol index 36ec030..558d5e3 100644 --- a/test/integration/MorphoBlue.t.sol +++ b/test/integration/MorphoBlue.t.sol @@ -95,7 +95,7 @@ contract MorphoBlueTests is MorphoTestBase { vm.warp(vm.getBlockTimestamp() + 365 days); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 49_292101); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 49_292100); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); @@ -188,7 +188,7 @@ contract MorphoBlueTests is MorphoTestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 4_994258); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 4_994256); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); @@ -222,7 +222,7 @@ contract MorphoBlueTests is MorphoTestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 77193); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 77192); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index f6688cd..e789a30 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -29,15 +29,15 @@ contract ProtocolIntegrationTests is TestBase { uint256 internal _excess; function setUp() external { + _deployV2Components(); + _migrate(); + _addToList(_EARNERS_LIST_NAME, _alice); _addToList(_EARNERS_LIST_NAME, _bob); _wrappedMToken.startEarningFor(_alice); _wrappedMToken.startEarningFor(_bob); - _deployV2Components(); - _migrate(); - _totalEarningSupplyOfM = _mToken.totalEarningSupply(); _wrapperBalanceOfM = _mToken.balanceOf(address(_wrappedMToken)); @@ -97,8 +97,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 99_999999); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); + assertEq(_wrappedMToken.excess(), _excess += 1); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -164,8 +164,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 199_999999); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); + assertEq(_wrappedMToken.excess(), _excess += 1); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -221,7 +221,7 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_423880); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_423881); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); @@ -264,8 +264,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); + assertEq(_wrappedMToken.excess(), _excess += 1); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -319,8 +319,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _bobBalance + 2_395361 - 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + 100_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361 - 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361 + 1); + assertEq(_wrappedMToken.excess(), _excess += 2); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -337,8 +337,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 50_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 50_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -390,8 +390,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 99_999999); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); + assertEq(_wrappedMToken.excess(), _excess += 1); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -419,8 +419,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -448,8 +448,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 99_999999); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _aliceBalance); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614473); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614473 + 1); + assertEq(_wrappedMToken.excess(), _excess += 1); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -465,8 +465,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _carolBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _carolBalance); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); @@ -525,8 +525,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361 + 1); + assertEq(_wrappedMToken.excess(), _excess += 1); assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 523d731..5e5ce8c 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -64,6 +64,44 @@ contract TestBase is Test { address internal _implementationV2; address internal _migratorV1; + 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 _getSource(address token_) internal pure returns (address source_) { if (token_ == _USDC) return _USDC_SOURCE; @@ -178,7 +216,14 @@ contract TestBase is Test { _implementationV2 = address( new WrappedMToken(address(_mToken), _registrar, _excessDestination, _migrationAdmin) ); - _migratorV1 = address(new MigratorV1(_implementationV2)); + + address[] memory earners_ = new address[](_earners.length); + + for (uint256 index_; index_ < _earners.length; ++index_) { + earners_[index_] = _earners[index_]; + } + + _migratorV1 = address(new MigratorV1(_implementationV2, earners_)); } function _migrate() internal { diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index eb79457..f3bcf34 100644 --- a/test/integration/UniswapV3.t.sol +++ b/test/integration/UniswapV3.t.sol @@ -338,7 +338,7 @@ 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 += 11_753_024234); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 11_753_024235); /* ============ Eric (Earner) Swaps Exact wM for USDC ============ */ diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index b8e1fd5..cb5e967 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -18,17 +18,62 @@ contract UpgradeTests is Test, DeployBase { uint64 internal constant _DEPLOYER_NONCE = 50; + 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 test_upgrade() external { vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); (address expectedImplementation_, address expectedMigrator_) = mockDeployUpgrade(_DEPLOYER, _DEPLOYER_NONCE); + address[] memory earners_ = new address[](_earners.length); + + for (uint256 index_; index_ < _earners.length; ++index_) { + earners_[index_] = _earners[index_]; + } + vm.startPrank(_DEPLOYER); (address implementation_, address migrator_) = deployUpgrade( _M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, - _MIGRATION_ADMIN + _MIGRATION_ADMIN, + earners_ ); vm.stopPrank(); diff --git a/test/unit/Migration.t.sol b/test/unit/Migration.t.sol index 6d81210..ea1a84f 100644 --- a/test/unit/Migration.t.sol +++ b/test/unit/Migration.t.sol @@ -45,7 +45,7 @@ contract MigrationTests is Test { function test_migration() external { WrappedMTokenV3 implementationV3_ = new WrappedMTokenV3(); - address migrator_ = address(new Migrator(address(implementationV3_))); + address migrator_ = address(new Migrator(address(implementationV3_), new address[](0))); _registrar.set( keccak256(abi.encode(_MIGRATOR_KEY_PREFIX, address(_wrappedMToken))), @@ -62,7 +62,7 @@ contract MigrationTests is Test { function test_migration_fromAdmin() external { WrappedMTokenV3 implementationV3_ = new WrappedMTokenV3(); - address migrator_ = address(new Migrator(address(implementationV3_))); + address migrator_ = address(new Migrator(address(implementationV3_), new address[](0))); vm.expectRevert(); WrappedMTokenV3(address(_wrappedMToken)).foo(); diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index 906a3a0..fd7d367 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -194,8 +194,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 300_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 300_000000); assertEq(_wrappedMToken.totalSupply(), 600_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 50_000001); - assertEq(_wrappedMToken.excess(), 249_999999); + assertEq(_wrappedMToken.totalAccruedYield(), 49_999998); + assertEq(_wrappedMToken.excess(), 250_000002); vm.prank(_dave); _wrappedMToken.transfer(_bob, 50_000000); @@ -212,8 +212,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalSupply(), 650_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 2); - assertEq(_wrappedMToken.excess(), 249_999996); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.excess(), 249_999999); _mToken.setCurrentIndex(4 * _EXP_SCALED_ONE); _mToken.setBalanceOf(address(_wrappedMToken), 1_200_000000); // was 900 @ 3.0, so 1200 @ 4.0 @@ -238,8 +238,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalSupply(), 650_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 133_333336); - assertEq(_wrappedMToken.excess(), 416_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 133_333328); + assertEq(_wrappedMToken.excess(), 416_666672); _registrar.setListContains(_EARNERS_LIST_NAME, _alice, false); @@ -253,8 +253,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 516_666664); assertEq(_wrappedMToken.totalSupply(), 716_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 66_666672); - assertEq(_wrappedMToken.excess(), 416_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 66_666664); + assertEq(_wrappedMToken.excess(), 416_666672); _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); @@ -268,8 +268,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 316_666664); assertEq(_wrappedMToken.totalSupply(), 716_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 66_666672); - assertEq(_wrappedMToken.excess(), 416_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 66_666664); + assertEq(_wrappedMToken.excess(), 416_666672); _mToken.setCurrentIndex(5 * _EXP_SCALED_ONE); _mToken.setBalanceOf(address(_wrappedMToken), 1_500_000000); // was 1200 @ 4.0, so 1500 @ 5.0 @@ -294,8 +294,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 316_666664); assertEq(_wrappedMToken.totalSupply(), 716_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 183_333340); - assertEq(_wrappedMToken.excess(), 599_999995); + assertEq(_wrappedMToken.totalAccruedYield(), 183_333330); + assertEq(_wrappedMToken.excess(), 600_000005); vm.prank(_alice); _wrappedMToken.unwrap(_alice, 266_666664); @@ -308,8 +308,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 400_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 450_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 183_333340); - assertEq(_wrappedMToken.excess(), 600_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 183_333330); + assertEq(_wrappedMToken.excess(), 600_000010); vm.prank(_bob); _wrappedMToken.unwrap(_bob, 333_333330); @@ -322,8 +322,8 @@ contract StoryTests is Test { 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(), 50_000000); + assertEq(_wrappedMToken.excess(), 600_000010); vm.prank(_carol); _wrappedMToken.unwrap(_carol, 250_000000); diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index c456131..f72cf3f 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -37,8 +37,6 @@ contract WrappedMTokenTests is Test { address[] internal _accounts = [_alice, _bob, _charlie, _david]; - uint128 internal _currentIndex; - MockM internal _mToken; MockRegistrar internal _registrar; WrappedMTokenHarness internal _implementation; @@ -48,7 +46,6 @@ contract WrappedMTokenTests is Test { _registrar = new MockRegistrar(); _mToken = new MockM(); - _mToken.setCurrentIndex(_EXP_SCALED_ONE); _implementation = new WrappedMTokenHarness( address(_mToken), @@ -58,8 +55,6 @@ contract WrappedMTokenTests is Test { ); _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); - - _mToken.setCurrentIndex(_currentIndex = 1_100000068703); } /* ============ constants ============ */ @@ -125,36 +120,63 @@ contract WrappedMTokenTests is Test { 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); assertEq(_wrappedMToken.internalWrap(_alice, _alice, 1_000), 1_000); - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - assertEq(_wrappedMToken.totalNonEarningSupply(), 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.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } - function test_internalWrap_toEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + function test_wrap_toEarner() external { + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); + _mToken.setBalanceOf(_alice, 1_002); - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); - _mToken.setBalanceOf(_alice, 1_002); + _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); assertEq(_wrappedMToken.internalWrap(_alice, _alice, 999), 999); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 999); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 909); - assertEq(_wrappedMToken.totalEarningSupply(), 999); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 1); @@ -162,22 +184,26 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.internalWrap(_alice, _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.earningPrincipalOf(_alice), 1_000 + 908 + 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999 + 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 910); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908 + 0); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999 + 1); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 2); assertEq(_wrappedMToken.internalWrap(_alice, _alice, 2), 2); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 1_002); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0 + 1); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999 + 1 + 2); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 912); - assertEq(_wrappedMToken.totalEarningSupply(), 1_002); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908 + 0 + 1); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999 + 1 + 2); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } /* ============ wrap ============ */ @@ -191,37 +217,25 @@ contract WrappedMTokenTests is Test { function testFuzz_wrap( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } + _setupIndexes(earningEnabled_, currentMIndex_); - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); - _wrappedMToken.setTotalEarningSupply(balance_); + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } + wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(_wrappedMToken.currentIndex()) - balanceWithYield_)); - 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); @@ -259,37 +273,25 @@ contract WrappedMTokenTests is Test { function testFuzz_wrap_entireBalance( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); + _setupIndexes(earningEnabled_, currentMIndex_); - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); - _wrappedMToken.setTotalEarningSupply(balance_); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(currentIndex_) - balance_)); + wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(_wrappedMToken.currentIndex()) - balanceWithYield_)); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); _mToken.setBalanceOf(_alice, wrapAmount_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -325,37 +327,25 @@ contract WrappedMTokenTests is Test { function testFuzz_wrapWithPermit_vrs( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } + _setupIndexes(earningEnabled_, currentMIndex_); - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); - _wrappedMToken.setTotalEarningSupply(balance_); + _setupAccount(_alice, accountEarning_, balanceWithYield_, 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_)); + wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(_wrappedMToken.currentIndex()) - balanceWithYield_)); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); _mToken.setBalanceOf(_alice, wrapAmount_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -391,37 +381,25 @@ contract WrappedMTokenTests is Test { function testFuzz_wrapWithPermit_signature( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); + _setupIndexes(earningEnabled_, currentMIndex_); - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); - _wrappedMToken.setTotalEarningSupply(balance_); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(currentIndex_) - balance_)); + wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(_wrappedMToken.currentIndex()) - balanceWithYield_)); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); _mToken.setBalanceOf(_alice, wrapAmount_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -461,55 +439,93 @@ contract WrappedMTokenTests is Test { } function test_internalUnwrap_insufficientBalance_fromEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 999, 909, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); _wrappedMToken.internalUnwrap(_alice, _alice, 1_000); } function test_internalUnwrap_fromNonEarner() external { + _mToken.setIsEarning(address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + + _mToken.setBalanceOf(address(_wrappedMToken), 1_000); + _wrappedMToken.setTotalNonEarningSupply(1_000); _wrappedMToken.setAccountOf(_alice, 1_000); - _mToken.setBalanceOf(address(_wrappedMToken), 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), 500); + emit IERC20.Transfer(_alice, address(0), 1); + + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 0); + + 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); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 500); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 499), 498); + 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.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 500); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 500); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 499); + 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.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_internalUnwrap_fromEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setIsEarning(address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); + _mToken.setBalanceOf(address(_wrappedMToken), 1_000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); - _wrappedMToken.setTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningPrincipal(909); + _wrappedMToken.setTotalEarningSupply(909); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 909, 909, false); // 999 balance with yield. - _mToken.setBalanceOf(address(_wrappedMToken), 1_000); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909); + assertEq(_wrappedMToken.balanceOf(_alice), 909); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 90); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 909); + assertEq(_wrappedMToken.totalEarningSupply(), 909); + assertEq(_wrappedMToken.totalAccruedYield(), 90); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 1); @@ -517,20 +533,39 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 0); // Change due to principal round up on unwrap. - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 999); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1); + assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 999); + assertEq(_wrappedMToken.totalEarningPrincipal(), 909 - 1); + assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.expectEmit(); - emit IERC20.Transfer(_alice, address(0), 999); + emit IERC20.Transfer(_alice, address(0), 498); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 999), 998); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 498), 497); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 0); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1 - 453); + assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1 - 498); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 909 - 1 - 453); + assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1 - 498); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, address(0), 500); + + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 499); + + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1 - 453 - 455); // 0 + assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1 - 498 - 500); // 0 + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 909 - 1 - 453 - 455); // 0 + assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1 - 498 - 500); // 0 + assertEq(_wrappedMToken.totalAccruedYield(), 0); } /* ============ unwrap ============ */ @@ -544,36 +579,22 @@ contract WrappedMTokenTests is Test { function testFuzz_unwrap( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, uint240 unwrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, 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_, false); - _wrappedMToken.setTotalEarningSupply(balance_); + _setupIndexes(earningEnabled_, currentMIndex_); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } - - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -614,35 +635,21 @@ contract WrappedMTokenTests is Test { function testFuzz_unwrap_entireBalance( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, 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_, false); - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } + _setupIndexes(earningEnabled_, currentMIndex_); - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -676,13 +683,16 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _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); @@ -692,22 +702,30 @@ contract WrappedMTokenTests is Test { 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 { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); _registrar.set( keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), bytes32(uint256(uint160(_bob))) ); - _wrappedMToken.enableEarning(); + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _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); @@ -720,37 +738,50 @@ contract WrappedMTokenTests is Test { 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(), 909); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } - function testFuzz_claimFor(uint240 balance_, uint128 accountIndex_, uint128 index_, bool claimOverride_) 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)); + function testFuzz_claimFor( + bool earningEnabled_, + bool accountEarning_, + uint240 balanceWithYield_, + uint240 balance_, + uint128 currentMIndex_, + bool claimOverride_ + ) external { + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _setupIndexes(earningEnabled_, currentMIndex_); + + (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))) + bytes32(uint256(uint160(_bob))) ); } - _wrappedMToken.enableEarning(); - - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); - - _mToken.setCurrentIndex(index_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); if (accruedYield_ != 0) { vm.expectEmit(); - emit IWrappedMToken.Claimed(_alice, claimOverride_ ? _charlie : _alice, accruedYield_); + emit IWrappedMToken.Claimed(_alice, claimOverride_ ? _bob : _alice, accruedYield_); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, accruedYield_); @@ -758,34 +789,41 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.claimFor(_alice), accruedYield_); - assertEq( - _wrappedMToken.totalSupply(), - _wrappedMToken.balanceOf(_alice) + _wrappedMToken.balanceOf(_bob) + _wrappedMToken.balanceOf(_charlie) - ); + assertEq(_wrappedMToken.totalSupply(), _wrappedMToken.balanceOf(_alice) + _wrappedMToken.balanceOf(_bob)); } /* ============ claimExcess ============ */ function testFuzz_claimExcess( - uint128 index_, + bool earningEnabled_, + uint128 currentMIndex_, uint240 totalNonEarningSupply_, - uint112 principalOfTotalEarningSupply_, + uint240 projectedTotalEarningSupply_, uint240 mBalance_ ) external { - index_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + + _setupIndexes(earningEnabled_, currentMIndex_); - totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, _getMaxAmount(index_))); + uint128 currentIndex_ = _wrappedMToken.currentIndex(); + uint240 maxAmount_ = _getMaxAmount(currentIndex_); - uint240 totalEarningSupply_ = uint112(bound(principalOfTotalEarningSupply_, 0, _getMaxAmount(index_))); + totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, maxAmount_)); - principalOfTotalEarningSupply_ = uint112(totalEarningSupply_ / index_); + projectedTotalEarningSupply_ = uint240( + bound(projectedTotalEarningSupply_, 0, maxAmount_ - totalNonEarningSupply_) + ); + + uint112 totalEarningPrincipal_ = IndexingMath.getPrincipalAmountRoundedUp( + projectedTotalEarningSupply_, + currentIndex_ + ); - mBalance_ = uint240(bound(mBalance_, totalNonEarningSupply_ + totalEarningSupply_, type(uint240).max)); + mBalance_ = uint240(bound(mBalance_, 0, maxAmount_)); _mToken.setBalanceOf(address(_wrappedMToken), mBalance_); - _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); - _wrappedMToken.setPrincipalOfTotalEarningSupply(principalOfTotalEarningSupply_); - _mToken.setCurrentIndex(index_); + _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); + _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); uint240 expectedExcess_ = _wrappedMToken.excess(); @@ -798,7 +836,6 @@ contract WrappedMTokenTests is Test { emit IWrappedMToken.ExcessClaimed(expectedExcess_); assertEq(_wrappedMToken.claimExcess(), expectedExcess_); - assertEq(_wrappedMToken.excess(), 0); } /* ============ transfer ============ */ @@ -811,6 +848,14 @@ contract WrappedMTokenTests is Test { _wrappedMToken.transfer(address(0), 1_000); } + 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); @@ -820,11 +865,10 @@ contract WrappedMTokenTests is Test { } function test_transfer_insufficientBalance_fromEarner_toNonEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 909, 909, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); vm.prank(_alice); @@ -848,8 +892,8 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_bob), 1_000); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_500); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); } function testFuzz_transfer_fromNonEarner_toNonEarner( @@ -857,7 +901,7 @@ contract WrappedMTokenTests is Test { uint256 aliceBalance_, uint256 transferAmount_ ) external { - supply_ = bound(supply_, 1, type(uint112).max); + supply_ = bound(supply_, 1, type(uint240).max); aliceBalance_ = bound(aliceBalance_, 1, supply_); transferAmount_ = bound(transferAmount_, 1, aliceBalance_); uint256 bobBalance = supply_ - aliceBalance_; @@ -877,36 +921,46 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_bob), bobBalance + transferAmount_); assertEq(_wrappedMToken.totalNonEarningSupply(), supply_); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); } function test_transfer_fromEarner_toNonEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); _wrappedMToken.setTotalNonEarningSupply(500); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); + _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 IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); vm.prank(_alice); _wrappedMToken.transfer(_bob, 500); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 545); + assertEq(_wrappedMToken.balanceOf(_alice), 600); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_bob), 1_000); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalEarningSupply(), 500); + assertEq(_wrappedMToken.totalEarningPrincipal(), 545); + assertEq(_wrappedMToken.totalEarningSupply(), 600); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 1); @@ -914,27 +968,37 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.transfer(_bob, 1); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 499); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 544); + assertEq(_wrappedMToken.balanceOf(_alice), 599); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_bob), 1_001); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_001); - assertEq(_wrappedMToken.totalEarningSupply(), 499); + assertEq(_wrappedMToken.totalEarningPrincipal(), 544); + assertEq(_wrappedMToken.totalEarningSupply(), 599); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_transfer_fromNonEarner_toEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(454); + _wrappedMToken.setTotalEarningPrincipal(500); _wrappedMToken.setTotalEarningSupply(500); _wrappedMToken.setTotalNonEarningSupply(1_000); _wrappedMToken.setAccountOf(_alice, 1_000); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex, false); + _wrappedMToken.setAccountOf(_bob, 500, 500, false); // 550 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_bob, _bob, 50); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _bob, 50); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -944,23 +1008,40 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 500); - assertEq(_wrappedMToken.lastIndexOf(_bob), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + assertEq(_wrappedMToken.earningPrincipalOf(_bob), 954); + assertEq(_wrappedMToken.balanceOf(_bob), 1_050); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 500); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalEarningPrincipal(), 954); + assertEq(_wrappedMToken.totalEarningSupply(), 1_050); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_transfer_fromEarner_toEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(1_363); + _wrappedMToken.setTotalEarningPrincipal(1_500); _wrappedMToken.setTotalEarningSupply(1_500); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex, false); + _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 IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_bob, _bob, 50); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _bob, 50); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -968,14 +1049,18 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.transfer(_bob, 500); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 545); + assertEq(_wrappedMToken.balanceOf(_alice), 600); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); - assertEq(_wrappedMToken.lastIndexOf(_bob), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + assertEq(_wrappedMToken.earningPrincipalOf(_bob), 954); + assertEq(_wrappedMToken.balanceOf(_bob), 1_050); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 1_500); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_499); + assertEq(_wrappedMToken.totalEarningSupply(), 1_650); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_transfer_nonEarnerToSelf() external { @@ -992,24 +1077,25 @@ contract WrappedMTokenTests is Test { 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_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); - - _mToken.setCurrentIndex((_currentIndex * 5) / 3); // 1_833333447838 + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 666); + 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 IERC20.Transfer(_alice, _alice, 500); @@ -1017,66 +1103,45 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.transfer(_alice, 500); - assertEq(_wrappedMToken.balanceOf(_alice), 1_666); + 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 testFuzz_transfer( bool earningEnabled_, bool aliceEarning_, bool bobEarning_, + uint240 aliceBalanceWithYield_, uint240 aliceBalance_, + uint240 bobBalanceWithYield_, uint240 bobBalance_, - uint128 aliceIndex_, - uint128 bobIndex_, - uint128 currentIndex_, + uint128 currentMIndex_, uint240 amount_ ) external { - aliceEarning_ = earningEnabled_ && aliceEarning_; - bobEarning_ = earningEnabled_ && bobEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, 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_, false); - _wrappedMToken.setTotalEarningSupply(aliceBalance_); + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(aliceBalance_, aliceIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, aliceBalance_); - _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); - } + _setupIndexes(earningEnabled_, currentMIndex_); - bobIndex_ = uint128(bound(bobIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - bobBalance_ = uint240(bound(bobBalance_, 0, _getMaxAmount(bobIndex_) / 4)); + (aliceBalanceWithYield_, aliceBalance_) = _getFuzzedBalances( + aliceBalanceWithYield_, + aliceBalance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - if (bobEarning_) { - _wrappedMToken.setAccountOf(_bob, bobBalance_, bobIndex_, false); - _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + bobBalance_); + _setupAccount(_alice, aliceEarning_, aliceBalanceWithYield_, aliceBalance_); - _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) + (bobBalanceWithYield_, bobBalance_) = _getFuzzedBalances( + bobBalanceWithYield_, + bobBalance_, + _getMaxAmount(_wrappedMToken.currentIndex()) - aliceBalanceWithYield_ ); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); + _setupAccount(_bob, bobEarning_, bobBalanceWithYield_, bobBalance_); uint240 aliceAccruedYield_ = _wrappedMToken.accruedYieldOf(_alice); uint240 bobAccruedYield_ = _wrappedMToken.accruedYieldOf(_bob); @@ -1112,15 +1177,12 @@ contract WrappedMTokenTests is Test { ); } else if (aliceEarning_) { assertEq(_wrappedMToken.totalEarningSupply(), aliceBalance_ + aliceAccruedYield_ - amount_); - assertEq(_wrappedMToken.totalNonEarningSupply(), bobBalance_ + bobAccruedYield_ + amount_); + assertEq(_wrappedMToken.totalNonEarningSupply(), bobBalance_ + amount_); } else if (bobEarning_) { - assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ + aliceAccruedYield_ - amount_); + assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ - amount_); assertEq(_wrappedMToken.totalEarningSupply(), bobBalance_ + bobAccruedYield_ + amount_); } else { - assertEq( - _wrappedMToken.totalNonEarningSupply(), - aliceBalance_ + aliceAccruedYield_ + bobBalance_ + bobAccruedYield_ - ); + assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ + bobBalance_); } } @@ -1131,22 +1193,17 @@ contract WrappedMTokenTests is Test { } function test_startEarningFor_notApprovedEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, _alice)); _wrappedMToken.startEarningFor(_alice); } function test_startEarning_overflow() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - uint256 aliceBalance_ = uint256(type(uint112).max) + 20; + _mToken.setCurrentIndex(1_000000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _mToken.setCurrentIndex(_currentIndex = _EXP_SCALED_ONE); + uint240 aliceBalance_ = uint240(type(uint112).max) + 1; _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); @@ -1159,9 +1216,8 @@ contract WrappedMTokenTests is Test { } function test_startEarningFor() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); _wrappedMToken.setTotalNonEarningSupply(1_000); @@ -1175,40 +1231,47 @@ contract WrappedMTokenTests is Test { _wrappedMToken.startEarningFor(_alice); assertEq(_wrappedMToken.isEarning(_alice), true); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909); assertEq(_wrappedMToken.balanceOf(_alice), 1000); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 909); assertEq(_wrappedMToken.totalEarningSupply(), 1_000); } - function testFuzz_startEarningFor(uint240 balance_, uint128 index_) external { - balance_ = uint240(bound(balance_, 0, _getMaxAmount(_currentIndex))); - index_ = uint128(bound(index_, _currentIndex, 10 * _EXP_SCALED_ONE)); + function testFuzz_startEarningFor(bool earningEnabled_, uint240 balance_, uint128 currentMIndex_) external { + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _setupIndexes(earningEnabled_, currentMIndex_); - _wrappedMToken.enableEarning(); + uint128 currentIndex_ = _wrappedMToken.currentIndex(); - _wrappedMToken.setTotalNonEarningSupply(balance_); + balance_ = uint240(bound(balance_, 0, _getMaxAmount(currentIndex_))); - _wrappedMToken.setAccountOf(_alice, balance_); + _setupAccount(_alice, false, balance_, balance_); _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _mToken.setCurrentIndex(index_); - - vm.expectEmit(); - emit IWrappedMToken.StartedEarning(_alice); + if (earningEnabled_) { + vm.expectEmit(); + emit IWrappedMToken.StartedEarning(_alice); + } else { + vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); + } _wrappedMToken.startEarningFor(_alice); + if (!earningEnabled_) return; + + uint112 earningPrincipal_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); + assertEq(_wrappedMToken.isEarning(_alice), true); - assertEq(_wrappedMToken.lastIndexOf(_alice), index_); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), earningPrincipal_); assertEq(_wrappedMToken.balanceOf(_alice), balance_); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningSupply(), balance_); + assertEq(_wrappedMToken.totalEarningPrincipal(), earningPrincipal_); } /* ============ stopEarningFor ============ */ @@ -1220,54 +1283,80 @@ contract WrappedMTokenTests is Test { } function test_stopEarningFor() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); + _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.balanceOf(_alice), 999); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.isEarning(_alice), false); - assertEq(_wrappedMToken.totalNonEarningSupply(), 999); - assertEq(_wrappedMToken.totalEarningSupply(), 1); + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_100); + assertEq(_wrappedMToken.totalEarningPrincipal(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } - 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)); + function testFuzz_stopEarningFor( + bool earningEnabled_, + uint240 balanceWithYield_, + uint240 balance_, + uint128 currentMIndex_ + ) external { + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _setupIndexes(earningEnabled_, currentMIndex_); - _wrappedMToken.enableEarning(); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _wrappedMToken.setTotalEarningSupply(balance_); + _setupAccount(_alice, true, balanceWithYield_, balance_); - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - _mToken.setCurrentIndex(index_); + if (accruedYield_ != 0) { + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, accruedYield_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); + 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 ============ */ @@ -1308,45 +1397,23 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); } - function test_enableEarning_earningCannotBeReenabled() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - vm.expectRevert(IWrappedMToken.EarningCannotBeReenabled.selector); - _wrappedMToken.enableEarning(); - } - function test_enableEarning() external { _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + vm.expectEmit(); - emit IWrappedMToken.EarningEnabled(_currentIndex); + emit IWrappedMToken.EarningEnabled(1_100000000000); _wrappedMToken.enableEarning(); + + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); } /* ============ disableEarning ============ */ function test_disableEarning_earningIsDisabled() external { vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); _wrappedMToken.disableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); - - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.disableEarning(); } function test_disableEarning_approvedEarner() external { @@ -1357,23 +1424,21 @@ contract WrappedMTokenTests is Test { } function test_disableEarning() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); vm.expectEmit(); - emit IWrappedMToken.EarningDisabled(_currentIndex); + emit IWrappedMToken.EarningDisabled(1_100000000000); _wrappedMToken.disableEarning(); + + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); } /* ============ balanceOf ============ */ function test_balanceOf_nonEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1383,38 +1448,40 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _mToken.setCurrentIndex(2 * _currentIndex); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } function test_balanceOf_earner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. + + assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); + _wrappedMToken.setEarningPrincipalOf(_alice, 1_000); // Earning principal has no bearing on balance. assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _mToken.setCurrentIndex(2 * _currentIndex); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); + _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 { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1424,38 +1491,36 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); - _mToken.setCurrentIndex(2 * _currentIndex); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); } function test_balanceWithYieldOf_earner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 550); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); - _mToken.setCurrentIndex(2 * _currentIndex); + _mToken.setCurrentIndex(1_210000000000); - assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 2_200); + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_210); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. - assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_815); } /* ============ accruedYieldOf ============ */ function test_accruedYieldOf_nonEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1465,42 +1530,41 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); - _mToken.setCurrentIndex(2 * _currentIndex); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); } function test_accruedYieldOf_earner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 50); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); - _mToken.setCurrentIndex(2 * _currentIndex); + _mToken.setCurrentIndex(1_210000000000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 1_200); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 210); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 815); } - /* ============ lastIndexOf ============ */ - function test_lastIndexOf() external { - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); + /* ============ earningPrincipalOf ============ */ + function test_earningPrincipalOf() external { + _wrappedMToken.setAccountOf(_alice, 0, 100, false); - assertEq(_wrappedMToken.lastIndexOf(_alice), _EXP_SCALED_ONE); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 100); - _wrappedMToken.setAccountOf(_alice, 0, 2 * _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 0, 200, false); - assertEq(_wrappedMToken.lastIndexOf(_alice), 2 * _EXP_SCALED_ONE); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 200); } /* ============ isEarning ============ */ @@ -1518,15 +1582,11 @@ contract WrappedMTokenTests is Test { function test_isEarningEnabled() external { assertFalse(_wrappedMToken.isEarningEnabled()); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); assertTrue(_wrappedMToken.isEarningEnabled()); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); assertFalse(_wrappedMToken.isEarningEnabled()); } @@ -1620,43 +1680,38 @@ contract WrappedMTokenTests is Test { /* ============ currentIndex ============ */ function test_currentIndex() external { - assertEq(_wrappedMToken.currentIndex(), 0); + assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); - _mToken.setCurrentIndex(2 * _EXP_SCALED_ONE); - - assertEq(_wrappedMToken.currentIndex(), 0); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.enableEarning(); + assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); - assertEq(_wrappedMToken.currentIndex(), 2 * _EXP_SCALED_ONE); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _mToken.setCurrentIndex(3 * _EXP_SCALED_ONE); + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); + _mToken.setCurrentIndex(1_210000000000); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); + assertEq(_wrappedMToken.currentIndex(), 1_210000000000); - _wrappedMToken.disableEarning(); + _wrappedMToken.pushEnableDisableEarningIndex(1_210000000000); - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); + assertEq(_wrappedMToken.currentIndex(), 1_210000000000); - _mToken.setCurrentIndex(4 * _EXP_SCALED_ONE); + _mToken.setCurrentIndex(1_331000000000); - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); + assertEq(_wrappedMToken.currentIndex(), 1_210000000000); } /* ============ excess ============ */ function test_excess() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); assertEq(_wrappedMToken.excess(), 0); _wrappedMToken.setTotalNonEarningSupply(1_000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); _mToken.setBalanceOf(address(_wrappedMToken), 2_100); @@ -1665,29 +1720,72 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(address(_wrappedMToken), 2_101); - assertEq(_wrappedMToken.excess(), 0); + assertEq(_wrappedMToken.excess(), 1); _mToken.setBalanceOf(address(_wrappedMToken), 2_102); - assertEq(_wrappedMToken.excess(), 1); + assertEq(_wrappedMToken.excess(), 2); _mToken.setBalanceOf(address(_wrappedMToken), 3_102); - assertEq(_wrappedMToken.excess(), 1_001); + assertEq(_wrappedMToken.excess(), 1_002); + + _mToken.setCurrentIndex(1_210000000000); + + assertEq(_wrappedMToken.excess(), 892); + } + + function testFuzz_excess( + bool earningEnabled_, + uint128 currentMIndex_, + uint240 totalNonEarningSupply_, + uint240 totalProjectedEarningSupply_, + uint240 mBalance_ + ) external { + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + + _setupIndexes(earningEnabled_, currentMIndex_); + + 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() + ); + + mBalance_ = uint240(bound(mBalance_, 0, maxAmount_)); + + _mToken.setBalanceOf(address(_wrappedMToken), mBalance_); + + _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); + _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); + + uint240 totalProjectedSupply_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; + + if (mBalance_ > totalProjectedSupply_) { + assertLe(_wrappedMToken.excess(), mBalance_ - totalProjectedSupply_); + } else { + assertEq(_wrappedMToken.excess(), 0); + } } /* ============ totalAccruedYield ============ */ function test_totalAccruedYield() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setTotalEarningPrincipal(909); _wrappedMToken.setTotalEarningSupply(1_000); assertEq(_wrappedMToken.totalAccruedYield(), 0); - _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningPrincipal(1_000); assertEq(_wrappedMToken.totalAccruedYield(), 100); @@ -1695,7 +1793,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 200); - _mToken.setCurrentIndex(_currentIndex = 1_210000000000); + _mToken.setCurrentIndex(1_210000000000); assertEq(_wrappedMToken.totalAccruedYield(), 310); } @@ -1712,4 +1810,47 @@ contract WrappedMTokenTests is Test { function _getMaxAmount(uint128 index_) internal pure returns (uint240 maxAmount_) { return (uint240(type(uint112).max) * index_) / _EXP_SCALED_ONE; } + + function _setupIndexes(bool earningEnabled_, uint128 currentMIndex_) internal { + _mToken.setCurrentIndex(currentMIndex_); + + if (earningEnabled_) { + _mToken.setIsEarning(address(_wrappedMToken), true); + _wrappedMToken.pushEnableDisableEarningIndex(_EXP_SCALED_ONE); + } + } + + 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/Invariants.sol b/test/utils/Invariants.sol index 9f1eff6..3293c95 100644 --- a/test/utils/Invariants.sol +++ b/test/utils/Invariants.sol @@ -90,26 +90,23 @@ library Invariants { // 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 0f1bf8c..a64aa78 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -48,6 +48,10 @@ contract MockM { currentIndex = currentIndex_; } + function setIsEarning(address account_, bool isEarning_) external { + isEarning[account_] = isEarning_; + } + function startEarning() external { isEarning[msg.sender] = true; } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index a85b766..a6f9249 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -28,12 +28,17 @@ contract WrappedMTokenHarness is WrappedMToken { _accounts[account_].isEarning = isEarning_; } - function setLastIndexOf(address account_, uint256 index_) external { - _accounts[account_].lastIndex = uint128(index_); + function setEarningPrincipalOf(address account_, uint256 earningPrincipal_) external { + _accounts[account_].earningPrincipal = uint112(earningPrincipal_); } - function setAccountOf(address account_, uint256 balance_, uint256 index_, bool hasClaimRecipient_) external { - _accounts[account_] = Account(true, uint240(balance_), uint128(index_), hasClaimRecipient_); + 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 { @@ -52,15 +57,19 @@ contract WrappedMTokenHarness is WrappedMToken { totalEarningSupply = uint240(totalEarningSupply_); } - function setPrincipalOfTotalEarningSupply(uint256 principalOfTotalEarningSupply_) external { - principalOfTotalEarningSupply = uint112(principalOfTotalEarningSupply_); + function setTotalEarningPrincipal(uint256 totalEarningPrincipal_) external { + totalEarningPrincipal = uint112(totalEarningPrincipal_); + } + + function pushEnableDisableEarningIndex(uint128 index_) external { + _enableDisableEarningIndices.push(index_); } function getAccountOf( address account_ - ) external view returns (bool isEarning_, uint240 balance_, uint128 index_, bool hasClaimRecipient_) { + ) external view returns (bool isEarning_, uint240 balance_, uint112 earningPrincipal_, bool hasClaimRecipient_) { Account storage account = _accounts[account_]; - return (account.isEarning, account.balance, account.lastIndex, account.hasClaimRecipient); + return (account.isEarning, account.balance, account.earningPrincipal, account.hasClaimRecipient); } function getInternalClaimRecipientOf(address account_) external view returns (address claimRecipient_) { From fb67a315920165212b236a10d2817c7743aa3f7d Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:05:08 -0500 Subject: [PATCH 05/27] feat: cheaper earner-earner/nonEarner-nonEarner transfers (#107) --- src/WrappedMToken.sol | 78 +++++++++++++++++++++++++++----- test/integration/UniswapV3.t.sol | 8 ++-- test/unit/WrappedMToken.t.sol | 4 +- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 56970b3..89b6499 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -439,7 +439,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { uint112 earningPrincipal_ = accountInfo_.earningPrincipal; uint112 principal_ = UIntMath.min112( - IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_), + IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_), // prevents `earningPrincipal` underflow. earningPrincipal_ ); @@ -503,23 +503,31 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (amount_ == 0) return; - if (sender_ == recipient_) { - uint240 balance_ = _accounts[sender_].balance; + Account storage senderInfo_ = _accounts[sender_]; + Account storage recipientInfo_ = _accounts[recipient_]; + + bool senderIsEarner_ = senderInfo_.isEarning; + bool recipientIsEarner_ = recipientInfo_.isEarning; - if (balance_ < amount_) revert InsufficientBalance(sender_, 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; } - // TODO: Don't touch globals if both are earning or non-earning. - - _accounts[sender_].isEarning - ? _subtractEarningAmount(sender_, amount_, currentIndex_) - : _subtractNonEarningAmount(sender_, amount_); + if (senderInfo_.balance < amount_) revert InsufficientBalance(sender_, senderInfo_.balance, amount_); - _accounts[recipient_].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_); } /** @@ -532,6 +540,51 @@ 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. @@ -555,6 +608,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { uint112 totalEarningPrincipal_ = totalEarningPrincipal; unchecked { + // `min240` and `min112` prevent `totalEarningSupply` and `totalEarningPrincipal` underflow respectively. totalEarningSupply = totalEarningSupply_ - UIntMath.min240(amount_, totalEarningSupply_); totalEarningPrincipal = totalEarningPrincipal_ - UIntMath.min112(principal_, totalEarningPrincipal_); } diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index f3bcf34..8aad314 100644 --- a/test/integration/UniswapV3.t.sol +++ b/test/integration/UniswapV3.t.sol @@ -261,7 +261,7 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += daveWrappedM_); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); + assertLe(_wrappedMToken.accruedYieldOf(_pool), 3); } function test_uniswapV3_exactInputOrOutputForEarnersAndNonEarners() public { @@ -356,7 +356,7 @@ contract UniswapV3IntegrationTests is TestBase { // Move 3 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 3 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 7_051_281772); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 7_051_281773); /* ============ Dave (Non-Earner) Swaps wM for Exact USDC ============ */ @@ -436,8 +436,8 @@ contract UniswapV3IntegrationTests is TestBase { // Move 10 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 10 days); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_317339); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_130_990918); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_317340); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_130_990919); /* ============ Dave (Non-Earner) Swaps Exact wM for USDC ============ */ diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index f72cf3f..76708b8 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -1053,12 +1053,12 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 600); assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); - assertEq(_wrappedMToken.earningPrincipalOf(_bob), 954); + assertEq(_wrappedMToken.earningPrincipalOf(_bob), 955); assertEq(_wrappedMToken.balanceOf(_bob), 1_050); assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningPrincipal(), 1_499); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_500); assertEq(_wrappedMToken.totalEarningSupply(), 1_650); assertEq(_wrappedMToken.totalAccruedYield(), 0); } From 79264af8470b183b46062a12f3a910f68dea5e8c Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:08:22 -0500 Subject: [PATCH 06/27] feat: favour user and track rounding error (#109) --- lib/common | 2 +- src/WrappedMToken.sol | 156 ++++++++++++--------- src/interfaces/IMTokenLike.sol | 7 + src/interfaces/IWrappedMToken.sol | 15 +- test/integration/Protocol.t.sol | 206 ++++++++++++++++++---------- test/integration/UniswapV3.t.sol | 2 +- test/integration/Upgrade.t.sol | 6 + test/unit/Stories.t.sol | 40 +++--- test/unit/WrappedMToken.t.sol | 113 ++++++++++----- test/utils/Invariants.sol | 5 +- test/utils/Mocks.sol | 5 + test/utils/WrappedMTokenHarness.sol | 4 + 12 files changed, 360 insertions(+), 201 deletions(-) diff --git a/lib/common b/lib/common index 5f5719e..36b3cc9 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 5f5719e4764288957f3adf6884ae7dc83d9a7fda +Subproject commit 36b3cc900dba907bafa2ca3f9d2fc9c00fabe805 diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 89b6499..c6e0349 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -77,6 +77,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken uint112 public totalEarningPrincipal; + /// @inheritdoc IWrappedMToken + int144 public roundingError; + /// @inheritdoc IWrappedMToken uint240 public totalEarningSupply; @@ -167,10 +170,17 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /// @inheritdoc IWrappedMToken - function claimExcess() external returns (uint240 excess_) { - emit ExcessClaimed(excess_ = excess()); + function claimExcess() external returns (uint240 claimed_) { + int248 excess_ = excess(); + + if (excess_ <= 0) revert NoExcess(); + + claimed_ = _getSafeTransferableM(address(this), uint240(uint248(excess_))); + + emit ExcessClaimed(claimed_); - IMTokenLike(mToken).transfer(excessDestination, excess_); + // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. + IMTokenLike(mToken).transfer(excessDestination, claimed_); } /// @inheritdoc IWrappedMToken @@ -299,20 +309,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /// @inheritdoc IWrappedMToken - function excess() public view returns (uint240 excess_) { - uint128 currentIndex_ = currentIndex(); - uint240 balance_ = _mBalanceOf(address(this)); - + function excess() public view returns (int248 excess_) { unchecked { - uint240 earmarked_ = totalNonEarningSupply + _projectedEarningSupply(currentIndex_); + int248 earmarked_ = int248(uint248(totalNonEarningSupply + projectedEarningSupply())) + roundingError; + int248 balance_ = int248(uint248(_mBalanceOf(address(this)))); - return balance_ > earmarked_ ? _getSafeTransferableM(balance_ - earmarked_, currentIndex_) : 0; + // The entire M balance is excess if the total projected supply (factoring rounding errors) is less than 0. + return earmarked_ <= 0 ? balance_ : balance_ - earmarked_; } } /// @inheritdoc IWrappedMToken function totalAccruedYield() external view returns (uint240 yield_) { - uint240 projectedEarningSupply_ = _projectedEarningSupply(currentIndex()); + uint240 projectedEarningSupply_ = projectedEarningSupply(); uint240 earningSupply_ = totalEarningSupply; unchecked { @@ -327,6 +336,15 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } } + /// @inheritdoc IWrappedMToken + function projectedEarningSupply() public view returns (uint240 supply_) { + return + UIntMath.max240( + IndexingMath.getPresentAmountRoundedUp(totalEarningPrincipal, currentIndex()), + totalEarningSupply + ); + } + /* ============ Internal Interactive Functions ============ */ /** @@ -342,8 +360,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { 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_); @@ -364,8 +380,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { 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_); @@ -431,15 +445,15 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { */ function _subtractEarningAmount(address account_, uint240 amount_, uint128 currentIndex_) internal { Account storage accountInfo_ = _accounts[account_]; - uint240 balance_ = accountInfo_.balance; if (balance_ < amount_) revert InsufficientBalance(account_, balance_, amount_); uint112 earningPrincipal_ = accountInfo_.earningPrincipal; + // `min112` prevents `earningPrincipal` underflow. uint112 principal_ = UIntMath.min112( - IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_), // prevents `earningPrincipal` underflow. + IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_), earningPrincipal_ ); @@ -622,16 +636,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @return wrapped_ The amount of wM minted. */ function _wrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 wrapped_) { - uint240 startingBalance_ = _mBalanceOf(address(this)); - - // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. - IMTokenLike(mToken).transferFrom(account_, 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 token. - _mint(recipient_, wrapped_ = _mBalanceOf(address(this)) - startingBalance_); + _transferFromM(account_, amount_); + _mint(recipient_, wrapped_ = amount_); } /** @@ -643,17 +649,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { */ function _unwrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 unwrapped_) { _burn(account_, amount_); - - uint240 startingBalance_ = _mBalanceOf(address(this)); - - // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. - IMTokenLike(mToken).transfer(recipient_, _getSafeTransferableM(amount_, currentIndex())); - - // 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 startingBalance_ - _mBalanceOf(address(this)); + _transferM(recipient_, unwrapped_ = amount_); } /** @@ -712,6 +708,48 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { emit StoppedEarning(account_); } + /** + * @dev Transfer `amount_` M to `recipient_`, tracking this contract's M balance rounding errors. + * @param recipient_ The account to transfer M to. + * @param amount_ The amount of M to transfer. + */ + function _transferM(address recipient_, uint240 amount_) internal { + uint240 startingBalance_ = _mBalanceOf(address(this)); + + // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. + IMTokenLike(mToken).transfer(recipient_, amount_); + + // 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 captured. + uint240 decrease_ = startingBalance_ - _mBalanceOf(address(this)); + + // If the M lost is more than the wM burned, then the difference is added to `roundingError`. + roundingError += int144(int256(uint256(decrease_)) - int256(uint256(amount_))); + } + + /** + * @dev Transfer `amount_` M from `sender_`, tracking this contract's M balance rounding errors. + * @param sender_ The account to transfer M from. + * @param amount_ The amount of M to transfer. + */ + function _transferFromM(address sender_, uint240 amount_) internal { + uint240 startingBalance_ = _mBalanceOf(address(this)); + + // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. + IMTokenLike(mToken).transferFrom(sender_, address(this), _getSafeTransferableM(sender_, 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 more/less than `amount_`. In order to capture the real increase in M, the difference between the + // starting and ending M balance is captured. + uint240 increase_ = _mBalanceOf(address(this)) - startingBalance_; + + // If the M gained is more/less than the wM minted, then the difference is subtracted/added to `roundingError`. + roundingError += int144(int256(uint256(amount_)) - int256(uint256(increase_))); + } + /* ============ Internal View/Pure Functions ============ */ /// @dev Returns the current index of the M Token. @@ -754,19 +792,26 @@ 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 this contract. - * @param currentIndex_ The current index. - * @return safeAmount_ The adjusted amount that can safely be transferred out. + * @param amount_ Some amount to be transferred out of this contract. + * @return safeAmount_ The adjusted amount that can safely be transferred out. */ - function _getSafeTransferableM(uint240 amount_, uint128 currentIndex_) internal view returns (uint240 safeAmount_) { - // If this contract 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 _getSafeTransferableM(address sender_, uint240 amount_) internal view returns (uint240 safeAmount_) { + // If `sender` is not earning, no need to adjust `amount_`. + if (!IMTokenLike(mToken).isEarning(sender_)) return amount_; + + uint128 currentIndex_ = _currentMIndex(); + uint112 startingPrincipal_ = uint112(IMTokenLike(mToken).principalBalanceOf(sender_)); + uint240 startingBalance_ = IndexingMath.getPresentAmountRoundedDown(startingPrincipal_, currentIndex_); + + // Adjust `amount_` to ensure it's M balance decrement is limited to `amount_`. + unchecked { + uint112 minEndingPrincipal_ = IndexingMath.getPrincipalAmountRoundedUp( + startingBalance_ - amount_, + currentIndex_ + ); + + return IndexingMath.getPresentAmountRoundedDown(startingPrincipal_ - minEndingPrincipal_, currentIndex_); + } } /// @dev Returns the address of the contract to use as a migrator, if any. @@ -801,19 +846,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return uint240(IMTokenLike(mToken).balanceOf(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. - */ - function _projectedEarningSupply(uint128 currentIndex_) internal view returns (uint240 supply_) { - return - UIntMath.max240( - IndexingMath.getPresentAmountRoundedDown(totalEarningPrincipal, currentIndex_), - totalEarningSupply - ); - } - /** * @dev Reverts if `amount_` is equal to 0. * @param amount_ Amount of token. diff --git a/src/interfaces/IMTokenLike.sol b/src/interfaces/IMTokenLike.sol index 15fb597..080e94d 100644 --- a/src/interfaces/IMTokenLike.sol +++ b/src/interfaces/IMTokenLike.sol @@ -80,4 +80,11 @@ 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 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/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index ef991ae..1f68e69 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -88,6 +88,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ error NotApprovedEarner(address account); + /// @notice Emitted when there is no excess to claim. + error NoExcess(); + /// @notice Emitted when the non-governance migrate function is called by a account other than the migration admin. error UnauthorizedMigration(); @@ -178,9 +181,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /** * @notice Claims any excess M of this contract. - * @return excess The amount of excess claimed. + * @return claimed The amount of excess claimed. */ - function claimExcess() external returns (uint240 excess); + function claimExcess() external returns (uint240 claimed); /// @notice Enables earning of Wrapped M if allowed by the Registrar and if it has never been done. function enableEarning() external; @@ -260,7 +263,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { function currentIndex() external view returns (uint128 index); /// @notice This contract's current excess M that is not earmarked for account balances or accrued yield. - function excess() external view returns (uint240 excess); + function excess() external view returns (int248 excess); /** * @notice Returns whether `account` is a wM earner. @@ -281,6 +284,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the M Token contract. function mToken() external view returns (address mToken); + /// @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); @@ -298,4 +304,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the destination where excess is claimed to. function excessDestination() external view returns (address excessDestination); + + /// @notice The amount of rounding error the contract has lost due to rounding in favour of users. + function roundingError() external view returns (int144 roundingError); } diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index e789a30..c39d9e2 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -26,7 +26,8 @@ contract ProtocolIntegrationTests is TestBase { uint256 internal _totalNonEarningSupply; uint256 internal _totalSupply; uint256 internal _totalAccruedYield; - uint256 internal _excess; + + int256 internal _excess; function setUp() external { _deployV2Components(); @@ -71,12 +72,12 @@ contract ProtocolIntegrationTests is TestBase { _wrapWithPermitVRS(_alice, _aliceKey, _alice, 100_000000, 0, block.timestamp); assertEq(_mToken.balanceOf(_alice), 100_000000); - assertEq(_wrappedMToken.balanceOf(_alice), 99_999999); + assertEq(_wrappedMToken.balanceOf(_alice), 100_000000); _wrapWithPermitSignature(_alice, _aliceKey, _alice, 100_000000, 1, block.timestamp); assertEq(_mToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.balanceOf(_alice), 199_999999); + assertEq(_wrappedMToken.balanceOf(_alice), 200_000000); } function test_integration_yieldAccumulation() external { @@ -91,16 +92,19 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 99_999999); // 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(), _totalEarningSupply += 99_999999); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); _giveM(_carol, 50_000000); @@ -121,7 +125,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 90 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 90 days); @@ -145,7 +152,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); _giveM(_bob, 200_000000); @@ -158,16 +168,19 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 199_999999); // 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(), _totalEarningSupply += 199_999999); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); _giveM(_dave, 150_000000); @@ -180,16 +193,19 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 149_999999); // Assert Dave (Non-Earner) - assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance = 149_999999); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance = 150_000000); assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); - assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 149_999999); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 150_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.excess(), _excess -= 2); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); assertEq(_wrappedMToken.claimFor(_alice), _aliceAccruedYield); @@ -207,7 +223,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1_190592); assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 180 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 180 days); @@ -239,7 +258,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); } function test_integration_yieldTransfer() external { @@ -249,7 +271,7 @@ 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); @@ -265,9 +287,12 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 180 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 180 days); @@ -320,9 +345,12 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _bobBalance + 2_395361 - 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361 + 1); - assertEq(_wrappedMToken.excess(), _excess += 2); + assertEq(_wrappedMToken.excess(), _excess += 1); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); _transferWM(_dave, _bob, 50_000000); @@ -340,7 +368,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 180 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 180 days); @@ -354,7 +385,7 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 57376); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 57377); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); @@ -372,7 +403,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); } function test_integration_yieldClaimUnwrap() external { @@ -382,18 +416,21 @@ contract ProtocolIntegrationTests is TestBase { _giveM(_carol, 100_000000); _wrap(_carol, _carol, 100_000000); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 99_999999); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 100_000000); assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += 100_000000); assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 199_999999); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 99_999999); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 180 days in the future to generate yield. vm.warp(vm.getBlockTimestamp() + 180 days); @@ -422,7 +459,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 90 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 90 days); @@ -446,12 +486,15 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 3_614473); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 99_999999); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614473 + 1); assertEq(_wrappedMToken.excess(), _excess += 1); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Start earning for Carol _addToList(_EARNERS_LIST_NAME, _carol); @@ -468,7 +511,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Fast forward 180 days in the future to generate yield vm.warp(vm.getBlockTimestamp() + 180 days); @@ -481,74 +527,89 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 2_423881); assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield += 2_395361); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); _unwrap(_alice, _alice, _aliceBalance); - // Assert Alice (Non-Earner) - assertEq(_mToken.balanceOf(_alice), 103_614471); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= 103_614472); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); - // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); - assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 103_614472); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _aliceBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + // Assert Alice (Non-Earner) + assertEq(_mToken.balanceOf(_alice), _aliceBalance); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= _aliceBalance); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + assertGe( + 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), 100_000000 + 3_614474 - 1); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= 100_000000); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= 3_614474); - // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _bobBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614474); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); + + // Assert Bob (Earner) + assertEq(_mToken.balanceOf(_bob), _bobBalance + _bobAccruedYield); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= _bobBalance); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= _bobAccruedYield); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); // Accrued yield of Carol is claimed when unwrapping _unwrap(_carol, _carol, _carolBalance + _carolAccruedYield); - // Assert Carol (Earner) - assertEq(_mToken.balanceOf(_carol), 100_000000 + 2_395361 - 1); - assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance -= 100_000000); - assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield -= 2_395361); - // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _carolBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361 + 1); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= _carolAccruedYield + 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + // Assert Carol (Earner) + assertEq(_mToken.balanceOf(_carol), _carolBalance + _carolAccruedYield); + assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance -= _carolBalance); + assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield -= _carolAccruedYield); - _unwrap(_dave, _dave, _daveBalance); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); - // Assert Dave (Non-Earner) - assertEq(_mToken.balanceOf(_dave), 100_000000 - 1); - assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance -= 100_000000); - assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); + _unwrap(_dave, _dave, _daveBalance); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); - assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 100_000000); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + // Assert Dave (Non-Earner) + assertEq(_mToken.balanceOf(_dave), _daveBalance); + assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance -= _daveBalance); + assertEq(_wrappedMToken.accruedYieldOf(_dave), 0); + + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); uint256 vaultStartingBalance_ = _mToken.balanceOf(_excessDestination); - assertEq(_wrappedMToken.claimExcess(), _excess); - assertEq(_mToken.balanceOf(_excessDestination), _excess + vaultStartingBalance_); + assertEq(_wrappedMToken.claimExcess(), uint256(_excess - 1)); + assertEq(_mToken.balanceOf(_excessDestination), uint256(_excess - 1) + vaultStartingBalance_); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); @@ -556,7 +617,10 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess -= _excess); - assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess); + assertGe( + int256(_wrapperBalanceOfM), + int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess + ); } function testFuzz_full(uint256 seed_) external { diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index 8aad314..b2b5a0d 100644 --- a/test/integration/UniswapV3.t.sol +++ b/test/integration/UniswapV3.t.sol @@ -50,7 +50,7 @@ contract UniswapV3IntegrationTests is TestBase { uint256 internal _poolAccruedYield; uint256 internal _bobAccruedYield; - uint240 internal _excess; + int248 internal _excess; function setUp() external { _deployV2Components(); diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index cb5e967..9e066fe 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -87,6 +87,8 @@ contract UpgradeTests is Test, DeployBase { // Migrator assertions assertEq(migrator_, expectedMigrator_); + uint240 totalEarningSupply_ = IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(); + vm.prank(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin()); IWrappedMToken(_WRAPPED_M_TOKEN).migrate(migrator_); @@ -96,5 +98,9 @@ contract UpgradeTests is Test, DeployBase { assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).registrar(), _REGISTRAR); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).excessDestination(), _EXCESS_DESTINATION); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).implementation(), implementation_); + + // Relevant storage slots. + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(), totalEarningSupply_); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).roundingError(), 0); } } diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index fd7d367..f9eaa71 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -177,7 +177,7 @@ contract StoryTests 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); @@ -195,7 +195,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 300_000000); assertEq(_wrappedMToken.totalSupply(), 600_000000); assertEq(_wrappedMToken.totalAccruedYield(), 49_999998); - assertEq(_wrappedMToken.excess(), 250_000002); + assertEq(_wrappedMToken.excess(), 250_000002); // TODO: fix rounding. vm.prank(_dave); _wrappedMToken.transfer(_bob, 50_000000); @@ -213,7 +213,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalSupply(), 650_000000); assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), 249_999999); + assertEq(_wrappedMToken.excess(), 250_000000); _mToken.setCurrentIndex(4 * _EXP_SCALED_ONE); _mToken.setBalanceOf(address(_wrappedMToken), 1_200_000000); // was 900 @ 3.0, so 1200 @ 4.0 @@ -295,7 +295,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 316_666664); assertEq(_wrappedMToken.totalSupply(), 716_666664); assertEq(_wrappedMToken.totalAccruedYield(), 183_333330); - assertEq(_wrappedMToken.excess(), 600_000005); + assertEq(_wrappedMToken.excess(), 600_000006); vm.prank(_alice); _wrappedMToken.unwrap(_alice, 266_666664); @@ -309,7 +309,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 450_000000); assertEq(_wrappedMToken.totalAccruedYield(), 183_333330); - assertEq(_wrappedMToken.excess(), 600_000010); + assertEq(_wrappedMToken.excess(), 600_000006); vm.prank(_bob); _wrappedMToken.unwrap(_bob, 333_333330); @@ -323,7 +323,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 250_000000); assertEq(_wrappedMToken.totalAccruedYield(), 50_000000); - assertEq(_wrappedMToken.excess(), 600_000010); + assertEq(_wrappedMToken.excess(), 600_000006); vm.prank(_carol); _wrappedMToken.unwrap(_carol, 250_000000); @@ -337,7 +337,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 50_000000); assertEq(_wrappedMToken.totalAccruedYield(), 0); - assertEq(_wrappedMToken.excess(), 600_000010); + assertEq(_wrappedMToken.excess(), 600_000006); vm.prank(_dave); _wrappedMToken.unwrap(_dave, 50_000000); @@ -351,16 +351,7 @@ contract StoryTests is Test { 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.excess(), 600_000006); } function test_noExcessCreep() external { @@ -380,19 +371,22 @@ contract StoryTests is Test { _wrappedMToken.wrap(_alice, 9); 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(_mToken.balanceOf(address(_wrappedMToken))) + ); vm.prank(_bob); _wrappedMToken.unwrap(_bob); @@ -415,8 +409,8 @@ contract StoryTests is Test { _wrappedMToken.wrap(_alice, 1); 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 index 76708b8..2ca10f2 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -470,7 +470,7 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 1); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 0); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 1); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 999); @@ -483,7 +483,7 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 499); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 499), 498); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 499), 499); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 500); @@ -496,7 +496,7 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 500); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 499); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 0); @@ -525,12 +525,12 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningPrincipal(), 909); assertEq(_wrappedMToken.totalEarningSupply(), 909); - assertEq(_wrappedMToken.totalAccruedYield(), 90); + assertEq(_wrappedMToken.totalAccruedYield(), 91); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 1); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 0); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 1); // Change due to principal round up on unwrap. assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1); @@ -539,12 +539,12 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningPrincipal(), 909 - 1); assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 91 - 90); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 498); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 498), 497); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 498), 498); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1 - 453); assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1 - 498); @@ -552,12 +552,12 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningPrincipal(), 909 - 1 - 453); assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1 - 498); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 91 - 90); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 500); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 499); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1 - 453 - 455); // 0 assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1 - 498 - 500); // 0 @@ -565,7 +565,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningPrincipal(), 909 - 1 - 453 - 455); // 0 assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1 - 498 - 500); // 0 - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 91 - 90 - 1); // 0 } /* ============ unwrap ============ */ @@ -797,45 +797,58 @@ contract WrappedMTokenTests is Test { bool earningEnabled_, uint128 currentMIndex_, uint240 totalNonEarningSupply_, - uint240 projectedTotalEarningSupply_, - uint240 mBalance_ + uint240 totalProjectedEarningSupply_, + uint112 mPrincipalBalance_, + int144 roundingError_ ) external { currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); _setupIndexes(earningEnabled_, currentMIndex_); - uint128 currentIndex_ = _wrappedMToken.currentIndex(); - uint240 maxAmount_ = _getMaxAmount(currentIndex_); + uint240 maxAmount_ = _getMaxAmount(_wrappedMToken.currentIndex()); totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, maxAmount_)); - projectedTotalEarningSupply_ = uint240( - bound(projectedTotalEarningSupply_, 0, maxAmount_ - totalNonEarningSupply_) + totalProjectedEarningSupply_ = uint240( + bound(totalProjectedEarningSupply_, 0, maxAmount_ - totalNonEarningSupply_) ); uint112 totalEarningPrincipal_ = IndexingMath.getPrincipalAmountRoundedUp( - projectedTotalEarningSupply_, - currentIndex_ + totalProjectedEarningSupply_, + _wrappedMToken.currentIndex() ); - mBalance_ = uint240(bound(mBalance_, 0, maxAmount_)); + 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 expectedExcess_ = _wrappedMToken.excess(); + roundingError_ = int144(bound(roundingError_, -1_000_000000, 1_000_000000)); - vm.expectCall( - address(_mToken), - abi.encodeCall(_mToken.transfer, (_wrappedMToken.excessDestination(), expectedExcess_)) - ); + _wrappedMToken.setRoundingError(roundingError_); - vm.expectEmit(); - emit IWrappedMToken.ExcessClaimed(expectedExcess_); + uint240 totalProjectedSupply_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; + int248 earmarked_ = int248(uint248(totalProjectedSupply_)) + roundingError_; + int248 excess_ = earmarked_ <= 0 ? int248(uint248(mBalance_)) : int248(uint248(mBalance_)) - earmarked_; + + if (excess_ <= 0) { + vm.expectRevert(IWrappedMToken.NoExcess.selector); + } else { + vm.expectEmit(false, false, false, false); + emit IWrappedMToken.ExcessClaimed(uint240(uint248(excess_))); + } + + uint240 claimed_ = _wrappedMToken.claimExcess(); - assertEq(_wrappedMToken.claimExcess(), expectedExcess_); + if (excess_ <= 0) return; + + assertLe(claimed_, uint248(excess_)); } /* ============ transfer ============ */ @@ -1718,21 +1731,41 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.excess(), 0); + _wrappedMToken.setRoundingError(1); + + assertEq(_wrappedMToken.excess(), -1); + _mToken.setBalanceOf(address(_wrappedMToken), 2_101); - assertEq(_wrappedMToken.excess(), 1); + assertEq(_wrappedMToken.excess(), 0); _mToken.setBalanceOf(address(_wrappedMToken), 2_102); - assertEq(_wrappedMToken.excess(), 2); + assertEq(_wrappedMToken.excess(), 1); _mToken.setBalanceOf(address(_wrappedMToken), 3_102); - assertEq(_wrappedMToken.excess(), 1_002); + assertEq(_wrappedMToken.excess(), 1_001); _mToken.setCurrentIndex(1_210000000000); + assertEq(_wrappedMToken.excess(), 891); + + _wrappedMToken.setRoundingError(0); + assertEq(_wrappedMToken.excess(), 892); + + _wrappedMToken.setRoundingError(-1); + + assertEq(_wrappedMToken.excess(), 893); + + _wrappedMToken.setRoundingError(-2_210); + + assertEq(_wrappedMToken.excess(), 3_102); + + _wrappedMToken.setRoundingError(-2_211); + + assertEq(_wrappedMToken.excess(), 3_102); } function testFuzz_excess( @@ -1740,7 +1773,8 @@ contract WrappedMTokenTests is Test { uint128 currentMIndex_, uint240 totalNonEarningSupply_, uint240 totalProjectedEarningSupply_, - uint240 mBalance_ + uint112 mPrincipalBalance_, + int144 roundingError_ ) external { currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); @@ -1759,20 +1793,25 @@ contract WrappedMTokenTests is Test { _wrappedMToken.currentIndex() ); - mBalance_ = uint240(bound(mBalance_, 0, maxAmount_)); + 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_); + roundingError_ = int144(bound(roundingError_, -1_000_000000, 1_000_000000)); + + _wrappedMToken.setRoundingError(roundingError_); + uint240 totalProjectedSupply_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; + int248 earmarked_ = int248(uint248(totalProjectedSupply_)) + roundingError_; - if (mBalance_ > totalProjectedSupply_) { - assertLe(_wrappedMToken.excess(), mBalance_ - totalProjectedSupply_); - } else { - assertEq(_wrappedMToken.excess(), 0); - } + assertLe(_wrappedMToken.excess(), int248(uint248(mBalance_)) - earmarked_); } /* ============ totalAccruedYield ============ */ diff --git a/test/utils/Invariants.sol b/test/utils/Invariants.sol index 3293c95..badd494 100644 --- a/test/utils/Invariants.sol +++ b/test/utils/Invariants.sol @@ -82,9 +82,8 @@ 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(); } diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index a64aa78..bb39b49 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -7,6 +7,7 @@ contract MockM { 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_, @@ -44,6 +45,10 @@ contract MockM { balanceOf[account_] = balance_; } + function setPrincipalBalanceOf(address account_, uint240 principal_) external { + principalBalanceOf[account_] = principal_; + } + function setCurrentIndex(uint128 currentIndex_) external { currentIndex = currentIndex_; } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index a6f9249..7f9a01f 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -65,6 +65,10 @@ contract WrappedMTokenHarness is WrappedMToken { _enableDisableEarningIndices.push(index_); } + function setRoundingError(int256 roundingError_) external { + roundingError = int144(roundingError_); + } + function getAccountOf( address account_ ) external view returns (bool isEarning_, uint240 balance_, uint112 earningPrincipal_, bool hasClaimRecipient_) { From a69eb5f8cd81801024ddc7308a3d6626d119757a Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:12:49 -0500 Subject: [PATCH 07/27] feat: No claim on wrap/unwrap/transfer (#105) --- src/WrappedMToken.sol | 36 ++--- test/integration/MorphoBlue.t.sol | 24 +-- test/integration/Protocol.t.sol | 46 +++--- test/integration/UniswapV3.t.sol | 96 ++++++------ test/unit/Stories.t.sol | 80 +++++----- test/unit/WrappedMToken.t.sol | 236 +++++++++++------------------- 6 files changed, 223 insertions(+), 295 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index c6e0349..60ab2ac 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -161,7 +161,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function unwrap(address recipient_) external returns (uint240 unwrapped_) { - return _unwrap(msg.sender, recipient_, uint240(balanceWithYieldOf(msg.sender))); + return _unwrap(msg.sender, recipient_, uint240(balanceOf(msg.sender))); } /// @inheritdoc IWrappedMToken @@ -264,7 +264,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /// @inheritdoc IWrappedMToken - function balanceWithYieldOf(address account_) public view returns (uint256 balance_) { + function balanceWithYieldOf(address account_) external view returns (uint256 balance_) { unchecked { return balanceOf(account_) + accruedYieldOf(account_); } @@ -356,14 +356,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { _revertIfInsufficientAmount(amount_); _revertIfInvalidRecipient(recipient_); - if (_accounts[recipient_].isEarning) { - uint128 currentIndex_ = currentIndex(); - - _claim(recipient_, currentIndex_); - _addEarningAmount(recipient_, amount_, currentIndex_); - } else { - _addNonEarningAmount(recipient_, amount_); - } + _accounts[recipient_].isEarning + ? _addEarningAmount(recipient_, amount_, currentIndex()) + : _addNonEarningAmount(recipient_, amount_); emit Transfer(address(0), recipient_, amount_); } @@ -376,14 +371,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_); - _subtractEarningAmount(account_, amount_, currentIndex_); - } else { - _subtractNonEarningAmount(account_, amount_); - } + _accounts[account_].isEarning + ? _subtractEarningAmount(account_, amount_, currentIndex()) + : _subtractNonEarningAmount(account_, amount_); emit Transfer(account_, address(0), amount_); } @@ -494,10 +484,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { 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_); } /** @@ -510,9 +499,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function _transfer(address sender_, address recipient_, uint240 amount_, uint128 currentIndex_) internal { _revertIfInvalidRecipient(recipient_); - _claim(sender_, currentIndex_); - _claim(recipient_, currentIndex_); - emit Transfer(sender_, recipient_, amount_); if (amount_ == 0) return; diff --git a/test/integration/MorphoBlue.t.sol b/test/integration/MorphoBlue.t.sol index 558d5e3..029ab2e 100644 --- a/test/integration/MorphoBlue.t.sol +++ b/test/integration/MorphoBlue.t.sol @@ -57,8 +57,8 @@ contract MorphoBlueTests is MorphoTestBase { // borrowing 0.90 USDC. _createMarket(_alice, _USDC); - // The market creation has triggered a wM transfer and the yield has been claimed for morpho. - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= _morphoAccruedYield); + // The market creation has triggered a wM transfer but the yield has not been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= 1); assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1e6); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 1e6); @@ -95,7 +95,7 @@ contract MorphoBlueTests is MorphoTestBase { vm.warp(vm.getBlockTimestamp() + 365 days); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 49_292100); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 49_292110); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); @@ -109,8 +109,8 @@ contract MorphoBlueTests is MorphoTestBase { _withdrawCollateral(_bob, address(_wrappedMToken), 1_000e6, _bob, _USDC); - // The collateral withdrawal has triggered a wM transfer and the yield has been claimed for morpho. - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= _morphoAccruedYield); + // The collateral withdrawal has triggered a wM transfer but the yield has not been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= 1); assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 1_000e6); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM -= 1_000e6); @@ -128,7 +128,7 @@ contract MorphoBlueTests is MorphoTestBase { vm.warp(vm.getBlockTimestamp() + 365 days); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 121446); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 2_545180); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); @@ -149,8 +149,8 @@ contract MorphoBlueTests is MorphoTestBase { // borrowing 0.90 wM. _createMarket(_alice, address(_wrappedMToken)); - // The market creation has triggered a wM transfer and the yield has been claimed for morpho. - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= _morphoAccruedYield); + // The market creation has triggered a wM transfer but the yield has not been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= 2); assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 100000); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 100000); @@ -188,7 +188,7 @@ contract MorphoBlueTests is MorphoTestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 4_994256); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 4_994266); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); @@ -197,8 +197,8 @@ contract MorphoBlueTests is MorphoTestBase { _repay(_bob, address(_wrappedMToken), 900e6, _USDC); - // The repay has triggered a wM transfer and the yield has been claimed for morpho. - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= _morphoAccruedYield); + // The repay has triggered a wM transfer but the yield has not been claimed for morpho. + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield); assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 900e6); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 900e6); @@ -222,7 +222,7 @@ contract MorphoBlueTests is MorphoTestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 77192); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 322772); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index c39d9e2..93cb894 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -333,19 +333,19 @@ contract ProtocolIntegrationTests is TestBase { // 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 + 2_395361 - 100_000000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 2_395361); + // Assert Globals + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _bobBalance - _aliceBalance); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + _aliceBalance); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); + assertEq(_wrappedMToken.excess(), _excess += 1); // 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(), _totalEarningSupply += _bobBalance + 2_395361 - 100_000000); - assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + 100_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361 + 1); - assertEq(_wrappedMToken.excess(), _excess += 1); + // Assert Alice (Earner) + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= _aliceBalance); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 1); assertGe( int256(_wrapperBalanceOfM), @@ -385,7 +385,7 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 57377); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 57378); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); @@ -550,38 +550,38 @@ contract ProtocolIntegrationTests is TestBase { int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); - // Accrued yield of Bob is claimed when unwrapping - _unwrap(_bob, _bob, _bobBalance + _bobAccruedYield); + // Accrued yield of Bob is not claimed when unwrapping + _unwrap(_bob, _bob, _bobBalance); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _bobBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614474); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess -= 2); // Assert Bob (Earner) - assertEq(_mToken.balanceOf(_bob), _bobBalance + _bobAccruedYield); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= _bobAccruedYield); + assertEq(_mToken.balanceOf(_bob), _bobBalance); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= 100_000000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= 1); assertGe( 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 -= _carolAccruedYield + 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); + assertEq(_wrappedMToken.excess(), _excess += 1); // Assert Carol (Earner) - assertEq(_mToken.balanceOf(_carol), _carolBalance + _carolAccruedYield); + assertEq(_mToken.balanceOf(_carol), _carolBalance); assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance -= _carolBalance); - assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield -= _carolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield); assertGe( int256(_wrapperBalanceOfM), @@ -594,7 +594,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.excess(), _excess -= 2); // Assert Dave (Non-Earner) assertEq(_mToken.balanceOf(_dave), _daveBalance); diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index b2b5a0d..07a487a 100644 --- a/test/integration/UniswapV3.t.sol +++ b/test/integration/UniswapV3.t.sol @@ -88,10 +88,10 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 999_930937); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); - // The mint has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); + // The mint 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 -= _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= 1); assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); @@ -103,7 +103,7 @@ contract UniswapV3IntegrationTests is TestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 878_557_430309); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 878_576_252249); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); @@ -121,11 +121,11 @@ contract UniswapV3IntegrationTests is TestBase { 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 for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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 ============ */ @@ -134,7 +134,7 @@ contract UniswapV3IntegrationTests is TestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 878_508_264721); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 921_727_256737); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); @@ -154,11 +154,11 @@ contract UniswapV3IntegrationTests is TestBase { 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 for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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 += 1_000e6); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1); } function testFuzz_uniswapV3_earning(uint256 aliceAmount_, uint256 bobUsdc_, uint256 daveWrappedM_) public { @@ -177,10 +177,8 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= amount0_); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += amount0_); - // The mint has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); - - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= _poolAccruedYield); + // The mint has triggered a wM transfer but the yield has not been claimed for the pool. + assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= amount1_); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += amount1_); @@ -198,11 +196,13 @@ contract UniswapV3IntegrationTests is TestBase { 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), _poolBalanceOfUSDC); @@ -217,11 +217,10 @@ contract UniswapV3IntegrationTests is TestBase { 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 for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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 ============ */ @@ -236,11 +235,13 @@ contract UniswapV3IntegrationTests is TestBase { 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), _poolBalanceOfUSDC); @@ -257,11 +258,10 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(IERC20(_USDC).balanceOf(_dave), swapOutUSDC_); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= swapOutUSDC_); - // The swap has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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_); - assertLe(_wrappedMToken.accruedYieldOf(_pool), 3); } function test_uniswapV3_exactInputOrOutputForEarnersAndNonEarners() public { @@ -280,10 +280,10 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 999_930937); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); - // The mint has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _poolAccruedYield); + // The mint 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 -= _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield -= 1); assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); @@ -293,7 +293,7 @@ contract UniswapV3IntegrationTests is TestBase { // Move 10 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 10 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_512_463128); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_512_966853); /* ============ 2 Non-Earners and 2 Earners are Initialized ============ */ @@ -314,17 +314,17 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); - // The swap has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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 -= _poolAccruedYield); + 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 += 2_349_986661); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 2_353_129323); // Claim yield for the pool and check that carol received yield. _wrappedMToken.claimFor(_pool); @@ -346,17 +346,17 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); - // The swap has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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 -= _poolAccruedYield); + 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 += 7_051_281773); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 7_055_919498); /* ============ Dave (Non-Earner) Swaps wM for Exact USDC ============ */ @@ -365,17 +365,17 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += daveOutput_); - // The swap has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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 -= _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); /* ============ 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 += 16_458_240233); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 16_475_562741); /* ============ Frank (Earner) Swaps wM for Exact USDC ============ */ @@ -383,10 +383,10 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += frankOutput_); - // The swap has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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 -= _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); } function test_uniswapV3_increaseDecreaseLiquidity() public { @@ -437,7 +437,7 @@ contract UniswapV3IntegrationTests is TestBase { vm.warp(vm.getBlockTimestamp() + 10 days); assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_317340); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_130_990919); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_514_282695); /* ============ Dave (Non-Earner) Swaps Exact wM for USDC ============ */ @@ -451,10 +451,10 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); - // The swap has triggered a wM transfer and the yield has been claimed for the pool. - assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM += _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 -= _poolAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); /* ============ Alice (Non-Earner) Decreases Liquidity And Bob (Earner) Increases Liquidity ============ */ diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index f9eaa71..d4d3b5c 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -183,48 +183,48 @@ contract StoryTests is Test { _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(), 49_999998); - assertEq(_wrappedMToken.excess(), 250_000002); // TODO: fix rounding. + assertEq(_wrappedMToken.totalSupply(), 500_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 149_999998); + assertEq(_wrappedMToken.excess(), 250_000002); 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(), 0); - assertEq(_wrappedMToken.excess(), 250_000000); + assertEq(_wrappedMToken.totalSupply(), 500_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 149_999996); + assertEq(_wrappedMToken.excess(), 250_000004); _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); @@ -235,10 +235,10 @@ contract StoryTests 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_333328); + assertEq(_wrappedMToken.totalSupply(), 500_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 283_333328); assertEq(_wrappedMToken.excess(), 416_666672); _registrar.setListContains(_EARNERS_LIST_NAME, _alice, false); @@ -250,10 +250,10 @@ contract StoryTests 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_666664); + assertEq(_wrappedMToken.totalSupply(), 666_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 116_666664); assertEq(_wrappedMToken.excess(), 416_666672); _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); @@ -265,10 +265,10 @@ contract StoryTests 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_666664); + assertEq(_wrappedMToken.totalSupply(), 666_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 116_666664); assertEq(_wrappedMToken.excess(), 416_666672); _mToken.setCurrentIndex(5 * _EXP_SCALED_ONE); @@ -279,8 +279,8 @@ contract StoryTests 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); @@ -291,10 +291,10 @@ contract StoryTests 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_333330); + assertEq(_wrappedMToken.totalSupply(), 666_666664); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); assertEq(_wrappedMToken.excess(), 600_000006); vm.prank(_alice); @@ -305,38 +305,38 @@ contract StoryTests is Test { 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_333330); + assertEq(_wrappedMToken.totalSupply(), 400_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); assertEq(_wrappedMToken.excess(), 600_000006); vm.prank(_bob); - _wrappedMToken.unwrap(_bob, 333_333330); + _wrappedMToken.unwrap(_bob, 150_000000); // 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_000000); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); assertEq(_wrappedMToken.excess(), 600_000006); vm.prank(_carol); - _wrappedMToken.unwrap(_carol, 250_000000); + _wrappedMToken.unwrap(_carol, 200_000000); // 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.totalAccruedYield(), 233_333330); assertEq(_wrappedMToken.excess(), 600_000006); vm.prank(_dave); @@ -350,7 +350,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalSupply(), 0); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); assertEq(_wrappedMToken.excess(), 600_000006); } diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 2ca10f2..7035589 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -171,12 +171,12 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.internalWrap(_alice, _alice, 999), 999); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908); - assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999); + assertEq(_wrappedMToken.totalAccruedYield(), 100); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 1); @@ -185,12 +185,12 @@ contract WrappedMTokenTests is Test { // No change due to principal round down on wrap. assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0); - assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999 + 1); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999 + 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 98); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908 + 0); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999 + 1); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999 + 1); + assertEq(_wrappedMToken.totalAccruedYield(), 99); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 2); @@ -198,12 +198,12 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.internalWrap(_alice, _alice, 2), 2); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0 + 1); - assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999 + 1 + 2); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + 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 + 1); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999 + 1 + 2); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999 + 1 + 2); + assertEq(_wrappedMToken.totalAccruedYield(), 98); } /* ============ wrap ============ */ @@ -238,8 +238,6 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(_alice, wrapAmount_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - if (wrapAmount_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); } else { @@ -252,7 +250,7 @@ contract WrappedMTokenTests is Test { if (wrapAmount_ == 0) return; - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ + wrapAmount_); + assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); assertEq( accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), @@ -294,8 +292,6 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(_alice, wrapAmount_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - if (wrapAmount_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); } else { @@ -308,7 +304,7 @@ contract WrappedMTokenTests is Test { if (wrapAmount_ == 0) return; - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ + wrapAmount_); + assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); assertEq( accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), @@ -348,8 +344,6 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(_alice, wrapAmount_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - if (wrapAmount_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); } else { @@ -362,7 +356,7 @@ contract WrappedMTokenTests is Test { if (wrapAmount_ == 0) return; - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ + wrapAmount_); + assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); assertEq( accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), @@ -402,8 +396,6 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(_alice, wrapAmount_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - if (wrapAmount_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); } else { @@ -416,7 +408,7 @@ contract WrappedMTokenTests is Test { if (wrapAmount_ == 0) return; - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ + wrapAmount_); + assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); assertEq( accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), @@ -514,18 +506,18 @@ contract WrappedMTokenTests is Test { _mToken.setBalanceOf(address(_wrappedMToken), 1_000); - _wrappedMToken.setTotalEarningPrincipal(909); - _wrappedMToken.setTotalEarningSupply(909); + _wrappedMToken.setTotalEarningPrincipal(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 909, 909, false); // 999 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. - assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909); - assertEq(_wrappedMToken.balanceOf(_alice), 909); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 90); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningPrincipal(), 909); - assertEq(_wrappedMToken.totalEarningSupply(), 909); - assertEq(_wrappedMToken.totalAccruedYield(), 91); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 100); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 1); @@ -533,39 +525,39 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 1); // Change due to principal round up on unwrap. - assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1); - assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + 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(), 909 - 1); - assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1); - assertEq(_wrappedMToken.totalAccruedYield(), 91 - 90); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 - 1); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1); + assertEq(_wrappedMToken.totalAccruedYield(), 100); vm.expectEmit(); - emit IERC20.Transfer(_alice, address(0), 498); + emit IERC20.Transfer(_alice, address(0), 499); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 498), 498); + assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 499), 499); - assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1 - 453); - assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1 - 498); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + 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(), 909 - 1 - 453); - assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1 - 498); - assertEq(_wrappedMToken.totalAccruedYield(), 91 - 90); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 - 1 - 454); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1 - 499); + assertEq(_wrappedMToken.totalAccruedYield(), 100); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 500); assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 500); - assertEq(_wrappedMToken.earningPrincipalOf(_alice), 909 - 1 - 453 - 455); // 0 - assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1 - 498 - 500); // 0 - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + 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(), 909 - 1 - 453 - 455); // 0 - assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1 - 498 - 500); // 0 - assertEq(_wrappedMToken.totalAccruedYield(), 91 - 90 - 1); // 0 + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 - 1 - 454 - 455); // 0 + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1 - 499 - 500); // 0 + assertEq(_wrappedMToken.totalAccruedYield(), 99); } /* ============ unwrap ============ */ @@ -596,22 +588,15 @@ contract WrappedMTokenTests is Test { _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); + _mToken.setBalanceOf(address(_wrappedMToken), balance_); - _mToken.setBalanceOf(address(_wrappedMToken), balance_ + accruedYield_); - - unwrapAmount_ = uint240(bound(unwrapAmount_, 0, (11 * (balance_ + accruedYield_)) / 10)); + unwrapAmount_ = uint240(bound(unwrapAmount_, 0, (11 * balance_) / 10)); if (unwrapAmount_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); - } else if (unwrapAmount_ > balance_ + accruedYield_) { + } else if (unwrapAmount_ > balance_) { vm.expectRevert( - abi.encodeWithSelector( - IWrappedMToken.InsufficientBalance.selector, - _alice, - balance_ + accruedYield_, - unwrapAmount_ - ) + abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, balance_, unwrapAmount_) ); } else { vm.expectEmit(); @@ -621,9 +606,9 @@ contract WrappedMTokenTests is Test { vm.startPrank(_alice); _wrappedMToken.unwrap(_alice, unwrapAmount_); - if ((unwrapAmount_ == 0) || (unwrapAmount_ > balance_ + accruedYield_)) return; + if ((unwrapAmount_ == 0) || (unwrapAmount_ > balance_)) return; - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_ - unwrapAmount_); + assertEq(_wrappedMToken.balanceOf(_alice), balance_ - unwrapAmount_); assertEq( accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), @@ -651,21 +636,19 @@ contract WrappedMTokenTests is Test { _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - - _mToken.setBalanceOf(address(_wrappedMToken), balance_ + accruedYield_); + _mToken.setBalanceOf(address(_wrappedMToken), balance_); - if (balance_ + accruedYield_ == 0) { + if (balance_ == 0) { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); } else { vm.expectEmit(); - emit IERC20.Transfer(_alice, address(0), balance_ + accruedYield_); + emit IERC20.Transfer(_alice, address(0), balance_); } vm.startPrank(_alice); _wrappedMToken.unwrap(_alice); - if (balance_ + accruedYield_ == 0) return; + if (balance_ == 0) return; assertEq(_wrappedMToken.balanceOf(_alice), 0); @@ -881,11 +864,11 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_100000000000); _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setAccountOf(_alice, 909, 909, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 1_000, 1_001)); vm.prank(_alice); - _wrappedMToken.transfer(_bob, 1_000); + _wrappedMToken.transfer(_bob, 1_001); } function test_transfer_fromNonEarner_toNonEarner() external { @@ -952,12 +935,6 @@ contract WrappedMTokenTests is Test { 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 IERC20.Transfer(_alice, _bob, 500); @@ -965,15 +942,15 @@ contract WrappedMTokenTests is Test { _wrappedMToken.transfer(_bob, 500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 545); - assertEq(_wrappedMToken.balanceOf(_alice), 600); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); assertEq(_wrappedMToken.balanceOf(_bob), 1_000); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); assertEq(_wrappedMToken.totalEarningPrincipal(), 545); - assertEq(_wrappedMToken.totalEarningSupply(), 600); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 500); + assertEq(_wrappedMToken.totalAccruedYield(), 100); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 1); @@ -982,15 +959,15 @@ contract WrappedMTokenTests is Test { _wrappedMToken.transfer(_bob, 1); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 544); - assertEq(_wrappedMToken.balanceOf(_alice), 599); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 499); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); assertEq(_wrappedMToken.balanceOf(_bob), 1_001); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_001); assertEq(_wrappedMToken.totalEarningPrincipal(), 544); - assertEq(_wrappedMToken.totalEarningSupply(), 599); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 499); + assertEq(_wrappedMToken.totalAccruedYield(), 100); } function test_transfer_fromNonEarner_toEarner() external { @@ -1007,12 +984,6 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); - vm.expectEmit(); - emit IWrappedMToken.Claimed(_bob, _bob, 50); - - vm.expectEmit(); - emit IERC20.Transfer(address(0), _bob, 50); - vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -1022,13 +993,13 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 500); assertEq(_wrappedMToken.earningPrincipalOf(_bob), 954); - assertEq(_wrappedMToken.balanceOf(_bob), 1_050); - assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 49); assertEq(_wrappedMToken.totalNonEarningSupply(), 500); assertEq(_wrappedMToken.totalEarningPrincipal(), 954); - assertEq(_wrappedMToken.totalEarningSupply(), 1_050); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 50); } function test_transfer_fromEarner_toEarner() external { @@ -1044,18 +1015,6 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); - vm.expectEmit(); - emit IWrappedMToken.Claimed(_alice, _alice, 100); - - vm.expectEmit(); - emit IERC20.Transfer(address(0), _alice, 100); - - vm.expectEmit(); - emit IWrappedMToken.Claimed(_bob, _bob, 50); - - vm.expectEmit(); - emit IERC20.Transfer(address(0), _bob, 50); - vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -1063,17 +1022,17 @@ contract WrappedMTokenTests is Test { _wrappedMToken.transfer(_bob, 500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 545); - assertEq(_wrappedMToken.balanceOf(_alice), 600); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 99); assertEq(_wrappedMToken.earningPrincipalOf(_bob), 955); - assertEq(_wrappedMToken.balanceOf(_bob), 1_050); - assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); + assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningPrincipal(), 1_500); - assertEq(_wrappedMToken.totalEarningSupply(), 1_650); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 1_500); + assertEq(_wrappedMToken.totalAccruedYield(), 150); } function test_transfer_nonEarnerToSelf() external { @@ -1104,12 +1063,6 @@ contract WrappedMTokenTests is Test { 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); - vm.expectEmit(); emit IERC20.Transfer(_alice, _alice, 500); @@ -1117,12 +1070,12 @@ contract WrappedMTokenTests is Test { _wrappedMToken.transfer(_alice, 500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000); - assertEq(_wrappedMToken.balanceOf(_alice), 1_100); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000); - assertEq(_wrappedMToken.totalEarningSupply(), 1_100); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 100); } function testFuzz_transfer( @@ -1156,19 +1109,11 @@ contract WrappedMTokenTests is Test { _setupAccount(_bob, bobEarning_, bobBalanceWithYield_, bobBalance_); - uint240 aliceAccruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - uint240 bobAccruedYield_ = _wrappedMToken.accruedYieldOf(_bob); + amount_ = uint240(bound(amount_, 0, (11 * aliceBalance_) / 10)); - amount_ = uint240(bound(amount_, 0, (11 * (aliceBalance_ + aliceAccruedYield_)) / 10)); - - if (amount_ > aliceBalance_ + aliceAccruedYield_) { + if (amount_ > aliceBalance_) { vm.expectRevert( - abi.encodeWithSelector( - IWrappedMToken.InsufficientBalance.selector, - _alice, - aliceBalance_ + aliceAccruedYield_, - amount_ - ) + abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, aliceBalance_, amount_) ); } else { vm.expectEmit(); @@ -1178,22 +1123,19 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.transfer(_bob, amount_); - if (amount_ > aliceBalance_ + aliceAccruedYield_) return; + if (amount_ > aliceBalance_) return; - assertEq(_wrappedMToken.balanceOf(_alice), aliceBalance_ + aliceAccruedYield_ - amount_); - assertEq(_wrappedMToken.balanceOf(_bob), bobBalance_ + bobAccruedYield_ + amount_); + assertEq(_wrappedMToken.balanceOf(_alice), aliceBalance_ - amount_); + assertEq(_wrappedMToken.balanceOf(_bob), bobBalance_ + amount_); if (aliceEarning_ && bobEarning_) { - assertEq( - _wrappedMToken.totalEarningSupply(), - aliceBalance_ + aliceAccruedYield_ + bobBalance_ + bobAccruedYield_ - ); + assertEq(_wrappedMToken.totalEarningSupply(), aliceBalance_ + bobBalance_); } else if (aliceEarning_) { - assertEq(_wrappedMToken.totalEarningSupply(), aliceBalance_ + aliceAccruedYield_ - amount_); + assertEq(_wrappedMToken.totalEarningSupply(), aliceBalance_ - amount_); assertEq(_wrappedMToken.totalNonEarningSupply(), bobBalance_ + amount_); } else if (bobEarning_) { assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ - amount_); - assertEq(_wrappedMToken.totalEarningSupply(), bobBalance_ + bobAccruedYield_ + amount_); + assertEq(_wrappedMToken.totalEarningSupply(), bobBalance_ + amount_); } else { assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ + bobBalance_); } From 7313eb9f3392b1d3ec87e044c7a95384b064826d Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:14:50 -0500 Subject: [PATCH 08/27] feat: Earner Manager (#100) --- script/Deploy.s.sol | 35 +- script/DeployBase.sol | 163 +++-- script/DeployUpgradeMainnet.s.sol | 44 +- src/EarnerManager.sol | 258 ++++++++ src/WrappedMToken.sol | 141 ++++- ...atorV1.sol => WrappedMTokenMigratorV1.sol} | 2 +- src/interfaces/IEarnerManager.sol | 178 ++++++ src/interfaces/IWrappedMToken.sol | 31 +- test/integration/Deploy.t.sol | 57 +- test/integration/TestBase.sol | 22 +- test/integration/Upgrade.t.sol | 60 +- test/unit/EarnerManager.sol | 598 ++++++++++++++++++ test/unit/Migration.t.sol | 75 --- test/unit/Migrations.t.sol | 140 ++++ test/unit/Stories.t.sol | 29 +- test/unit/WrappedMToken.t.sol | 290 +++++++-- test/utils/EarnerManagerHarness.sol | 17 + test/utils/Mocks.sol | 20 + test/utils/WrappedMTokenHarness.sol | 40 +- 19 files changed, 1914 insertions(+), 286 deletions(-) create mode 100644 src/EarnerManager.sol rename src/{MigratorV1.sol => WrappedMTokenMigratorV1.sol} (98%) create mode 100644 src/interfaces/IEarnerManager.sol create mode 100644 test/unit/EarnerManager.sol delete mode 100644 test/unit/Migration.t.sol create mode 100644 test/unit/Migrations.t.sol create mode 100644 test/utils/EarnerManagerHarness.sol diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 4bfec93..5e373f6 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -23,9 +23,9 @@ contract DeployProduction is Script, DeployBase { address deployer_ = vm.rememberKey(vm.envUint("PRIVATE_KEY")); address expectedDeployer_ = vm.envAddress("DEPLOYER"); - uint64 deployerProxyNonce_ = uint64(vm.envUint("DEPLOYER_PROXY_NONCE")); + uint64 deployerWrappedMProxyNonce_ = uint64(vm.envUint("DEPLOYER_WRAPPED_M_PROXY_NONCE")); - address expectedProxy_ = vm.envAddress("EXPECTED_PROXY"); + address expectedWrappedMProxy_ = vm.envAddress("EXPECTED_WRAPPED_M_PROXY"); console2.log("Deployer:", deployer_); @@ -34,15 +34,22 @@ contract DeployProduction is Script, DeployBase { uint64 currentNonce_ = vm.getNonce(deployer_); uint64 startNonce_ = currentNonce_; - address implementation_; - address proxy_; + address earnerManagerImplementation_; + address earnerManagerProxy_; + address wrappedMTokenImplementation_; + address wrappedMTokenProxy_; while (true) { - if (startNonce_ > deployerProxyNonce_) revert DeployerNonceTooHigh(); + if (startNonce_ > deployerWrappedMProxyNonce_) revert DeployerNonceTooHigh(); - (implementation_, proxy_) = mockDeploy(deployer_, startNonce_); + ( + earnerManagerImplementation_, + earnerManagerProxy_, + wrappedMTokenImplementation_, + wrappedMTokenProxy_ + ) = mockDeploy(deployer_, startNonce_); - if (proxy_ == expectedProxy_) break; + if (wrappedMTokenProxy_ == expectedWrappedMProxy_) break; ++startNonce_; } @@ -59,18 +66,22 @@ contract DeployProduction is Script, DeployBase { if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); - (implementation_, proxy_) = deploy( + (earnerManagerImplementation_, earnerManagerProxy_, wrappedMTokenImplementation_, wrappedMTokenProxy_) = deploy( vm.envAddress("M_TOKEN"), vm.envAddress("REGISTRAR"), vm.envAddress("EXCESS_DESTINATION"), - vm.envAddress("MIGRATION_ADMIN") + vm.envAddress("WRAPPED_M_MIGRATION_ADMIN"), + vm.envAddress("EARNER_MANAGER_MIGRATION_ADMIN") ); vm.stopBroadcast(); - console2.log("Wrapped M Implementation address:", implementation_); - console2.log("Wrapped M Proxy address:", proxy_); + console2.log("Wrapped M Implementation address:", wrappedMTokenImplementation_); + console2.log("Wrapped M Proxy address:", wrappedMTokenProxy_); + console2.log("Earner Manager Implementation address:", earnerManagerImplementation_); + console2.log("Earner Manager Proxy address:", earnerManagerProxy_); - if (proxy_ != expectedProxy_) revert ResultingProxyMismatch(expectedProxy_, proxy_); + if (wrappedMTokenProxy_ != expectedWrappedMProxy_) + revert ResultingProxyMismatch(expectedWrappedMProxy_, wrappedMTokenProxy_); } } diff --git a/script/DeployBase.sol b/script/DeployBase.sol index a6a6411..a68ddb9 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -5,88 +5,165 @@ pragma solidity 0.8.26; import { ContractHelper } from "../lib/common/src/libs/ContractHelper.sol"; import { Proxy } from "../lib/common/src/Proxy.sol"; -import { MigratorV1 } from "../src/MigratorV1.sol"; +import { EarnerManager } from "../src/EarnerManager.sol"; +import { WrappedMTokenMigratorV1 } from "../src/WrappedMTokenMigratorV1.sol"; import { WrappedMToken } from "../src/WrappedMToken.sol"; contract DeployBase { /** * @dev Deploys Wrapped M Token. - * @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 migrationAdmin_ The address of 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 excessDestination_ The address of the excess destination. + * @param wrappedMMigrationAdmin_ The address of the Wrapped M Migration Admin. + * @param earnerManagerMigrationAdmin_ The address of the Earner Manager Migration Admin. + * @return earnerManagerImplementation_ The address of the deployed Earner Manager implementation. + * @return earnerManagerProxy_ The address of the deployed Earner Manager proxy. + * @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 registrar_, address excessDestination_, - address migrationAdmin_ - ) public virtual returns (address implementation_, address proxy_) { - // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. - // Proxy needs `implementation_` addresses. + address wrappedMMigrationAdmin_, + address earnerManagerMigrationAdmin_ + ) + public + virtual + returns ( + address earnerManagerImplementation_, + address earnerManagerProxy_, + address wrappedMTokenImplementation_, + address wrappedMTokenProxy_ + ) + { + // Earner Manager Implementation constructor needs only known values. + // Earner Manager Proxy constructor needs `earnerManagerImplementation_`. + // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. + // Wrapped M Token Proxy constructor needs `wrappedMTokenImplementation_`. - implementation_ = address(new WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_)); - proxy_ = address(new Proxy(implementation_)); + earnerManagerImplementation_ = address(new EarnerManager(registrar_, earnerManagerMigrationAdmin_)); + + earnerManagerProxy_ = address(new Proxy(earnerManagerImplementation_)); + + wrappedMTokenImplementation_ = address( + new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, wrappedMMigrationAdmin_) + ); + + wrappedMTokenProxy_ = address(new Proxy(wrappedMTokenImplementation_)); } /** * @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 migrationAdmin_ The address of the Migration Admin. - * @return implementation_ The address of the deployed Wrapped M Token implementation. - * @return migrator_ The address of the deployed Migrator. + * @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 wrappedMMigrationAdmin_ The address of the Wrapped M Migration Admin. + * @param earnerManagerMigrationAdmin_ The address of the Earner Manager Migration Admin. + * @return earnerManagerImplementation_ The address of the deployed Earner Manager implementation. + * @return earnerManagerProxy_ The address of the deployed Earner Manager proxy. + * @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 migrationAdmin_, + address wrappedMMigrationAdmin_, + address earnerManagerMigrationAdmin_, address[] memory earners_ - ) public virtual returns (address implementation_, address migrator_) { - // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. - // Migrator needs `implementation_` addresses. + ) + public + virtual + returns ( + address earnerManagerImplementation_, + address earnerManagerProxy_, + address wrappedMTokenImplementation_, + address wrappedMTokenMigrator_ + ) + { + // Earner Manager Implementation constructor needs only known values. + // Earner Manager Proxy constructor needs `earnerManagerImplementation_`. + // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. + // Migrator needs `wrappedMTokenImplementation_` addresses. + + earnerManagerImplementation_ = address(new EarnerManager(registrar_, earnerManagerMigrationAdmin_)); + + earnerManagerProxy_ = address(new Proxy(earnerManagerImplementation_)); + + wrappedMTokenImplementation_ = address( + new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, wrappedMMigrationAdmin_) + ); - implementation_ = address(new WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_)); - migrator_ = address(new MigratorV1(implementation_, earners_)); + wrappedMTokenMigrator_ = address(new WrappedMTokenMigratorV1(wrappedMTokenImplementation_, earners_)); } /** * @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 implementation_ The address of the would-be Wrapped M Token implementation. - * @return proxy_ The address of the would-be Wrapped M Token proxy. + * @param deployer_ The address of the deployer. + * @param deployerNonce_ The nonce of the deployer. + * @return earnerManagerImplementation_ The address of the would-be Earner Manager implementation. + * @return earnerManagerProxy_ The address of the would-be Earner Manager proxy. + * @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 view virtual returns (address implementation_, address proxy_) { - // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. - // Proxy needs `implementation_` addresses. + ) + public + view + virtual + returns ( + address earnerManagerImplementation_, + address earnerManagerProxy_, + address wrappedMTokenImplementation_, + address wrappedMTokenProxy_ + ) + { + // Earner Manager Implementation constructor needs only known values. + // Earner Manager Proxy constructor needs `earnerManagerImplementation_`. + // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. + // Wrapped M Token Proxy constructor needs `wrappedMTokenImplementation_`. - implementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); - proxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); + earnerManagerImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + earnerManagerProxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); + wrappedMTokenImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 2); + wrappedMTokenProxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 3); } /** * @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 implementation_ The address of the would-be Wrapped M Token implementation. - * @return migrator_ The address of the would-be Migrator. + * @param deployer_ The address of the deployer. + * @param deployerNonce_ The nonce of the deployer. + * @return earnerManagerImplementation_ The address of the would-be Earner Manager implementation. + * @return earnerManagerProxy_ The address of the would-be Earner Manager proxy. + * @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 implementation_, address migrator_) { - // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. - // Migrator needs `implementation_` addresses. + ) + public + view + virtual + returns ( + address earnerManagerImplementation_, + address earnerManagerProxy_, + address wrappedMTokenImplementation_, + address wrappedMTokenMigrator_ + ) + { + // Earner Manager Implementation constructor needs only known values. + // Earner Manager Proxy constructor needs `earnerManagerImplementation_`. + // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. + // Migrator needs `wrappedMTokenImplementation_` addresses. - implementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); - migrator_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); + earnerManagerImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + earnerManagerProxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); + wrappedMTokenImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 2); + wrappedMTokenMigrator_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 3); } } diff --git a/script/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol index ad0fd17..cbec01c 100644 --- a/script/DeployUpgradeMainnet.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -25,9 +25,12 @@ contract DeployUpgradeMainnet is Script, DeployBase { address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; // NOTE: Ensure this is the correct Migration Admin mainnet address. - address internal constant _MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; - address internal constant _PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; // Mainnet address for the Proxy. + // NOTE: Ensure this is the correct Migration Admin mainnet address. + address internal constant _EARNER_MANAGER_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + + address internal constant _WRAPPED_M_PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; // Mainnet address for the Proxy. // NOTE: Ensure this is the correct mainnet deployer to use. address internal constant _EXPECTED_DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; @@ -36,7 +39,7 @@ contract DeployUpgradeMainnet is Script, DeployBase { uint64 internal constant _DEPLOYER_MIGRATOR_NONCE = 40; // NOTE: Ensure this is the correct expected mainnet address for the Migrator. - address internal constant _EXPECTED_MIGRATOR = address(0); + address internal constant _EXPECTED_WRAPPED_M_MIGRATOR = address(0); // NOTE: Ensure this is the correct and complete list of earners on mainnet. address[] internal _earners = [ @@ -87,15 +90,22 @@ contract DeployUpgradeMainnet is Script, DeployBase { uint64 currentNonce_ = vm.getNonce(deployer_); uint64 startNonce_ = currentNonce_; - address implementation_; - address migrator_; + address earnerManagerImplementation_; + address earnerManagerProxy_; + address wrappedMTokenImplementation_; + address wrappedMTokenMigrator_; while (true) { if (startNonce_ > _DEPLOYER_MIGRATOR_NONCE) revert DeployerNonceTooHigh(); - (implementation_, migrator_) = mockDeployUpgrade(deployer_, startNonce_); + ( + earnerManagerImplementation_, + earnerManagerProxy_, + wrappedMTokenImplementation_, + wrappedMTokenMigrator_ + ) = mockDeployUpgrade(deployer_, startNonce_); - if (migrator_ == _EXPECTED_MIGRATOR) break; + if (wrappedMTokenMigrator_ == _EXPECTED_WRAPPED_M_MIGRATOR) break; ++startNonce_; } @@ -118,19 +128,29 @@ contract DeployUpgradeMainnet is Script, DeployBase { earners_[index_] = _earners[index_]; } - (implementation_, migrator_) = deployUpgrade( + ( + earnerManagerImplementation_, + earnerManagerProxy_, + wrappedMTokenImplementation_, + wrappedMTokenMigrator_ + ) = deployUpgrade( _M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, - _MIGRATION_ADMIN, + _WRAPPED_M_MIGRATION_ADMIN, + _EARNER_MANAGER_MIGRATION_ADMIN, earners_ ); vm.stopBroadcast(); - console2.log("Wrapped M Implementation address:", implementation_); - console2.log("Migrator address:", migrator_); + console2.log("Earner Manager Implementation address:", earnerManagerImplementation_); + console2.log("Earner Manager Proxy address:", earnerManagerProxy_); + console2.log("Wrapped M Implementation address:", wrappedMTokenImplementation_); + console2.log("Migrator address:", wrappedMTokenMigrator_); - if (migrator_ != _EXPECTED_MIGRATOR) revert ResultingMigratorMismatch(_EXPECTED_MIGRATOR, migrator_); + if (wrappedMTokenMigrator_ != _EXPECTED_WRAPPED_M_MIGRATOR) { + revert ResultingMigratorMismatch(_EXPECTED_WRAPPED_M_MIGRATOR, wrappedMTokenMigrator_); + } } } diff --git a/src/EarnerManager.sol b/src/EarnerManager.sol new file mode 100644 index 0000000..3859a07 --- /dev/null +++ b/src/EarnerManager.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.26; + +import { Migratable } from "../lib/common/src/Migratable.sol"; + +import { IEarnerManager } from "./interfaces/IEarnerManager.sol"; +import { IRegistrarLike } from "./interfaces/IRegistrarLike.sol"; + +/** + * @title Earner Manager allows admins to define earners without governance, and take fees from yield. + * @author M^0 Labs + */ +contract EarnerManager is IEarnerManager, Migratable { + /* ============ Structs ============ */ + + struct EarnerDetails { + address admin; + uint16 feeRate; + } + + /* ============ Variables ============ */ + + /// @inheritdoc IEarnerManager + uint16 public constant MAX_FEE_RATE = 10_000; + + /// @inheritdoc IEarnerManager + bytes32 public constant ADMINS_LIST_NAME = "em_admins"; + + /// @inheritdoc IEarnerManager + bytes32 public constant EARNERS_LIST_IGNORED_KEY = "earners_list_ignored"; + + /// @inheritdoc IEarnerManager + bytes32 public constant EARNERS_LIST_NAME = "earners"; + + /// @inheritdoc IEarnerManager + bytes32 public constant MIGRATOR_KEY_PREFIX = "em_migrator_v1"; + + /// @inheritdoc IEarnerManager + address public immutable registrar; + + /// @inheritdoc IEarnerManager + address public immutable migrationAdmin; + + /// @dev Mapping of account to earner details. + mapping(address account => EarnerDetails earnerDetails) internal _earnerDetails; + + /* ============ Modifiers ============ */ + + modifier onlyAdmin() { + _revertIfNotAdmin(); + _; + } + + /* ============ Constructor ============ */ + + /** + * @dev Constructs the contract. + * @param registrar_ The address of a Registrar contract. + * @param migrationAdmin_ The address of a migration admin. + */ + constructor(address registrar_, address migrationAdmin_) { + if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); + if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); + } + + /* ============ Interactive Functions ============ */ + + /// @inheritdoc IEarnerManager + function setEarnerDetails(address account_, bool status_, uint16 feeRate_) external onlyAdmin { + if (earnersListsIgnored()) revert EarnersListsIgnored(); + + _setDetails(account_, status_, feeRate_); + } + + /// @inheritdoc IEarnerManager + function setEarnerDetails( + address[] calldata accounts_, + bool[] calldata statuses_, + uint16[] calldata feeRates_ + ) external onlyAdmin { + if (accounts_.length == 0) revert ArrayLengthZero(); + if (accounts_.length != statuses_.length) revert ArrayLengthMismatch(); + if (accounts_.length != feeRates_.length) revert ArrayLengthMismatch(); + if (earnersListsIgnored()) revert EarnersListsIgnored(); + + for (uint256 index_; index_ < accounts_.length; ++index_) { + // NOTE: The `isAdmin` check in `_setDetails` will make this costly to re-set details for multiple accounts + // that have already been set by the same admin, due to the redundant queries to the registrar. + // Consider transient storage in `isAdmin` to memoize admins. + _setDetails(accounts_[index_], statuses_[index_], feeRates_[index_]); + } + } + + /* ============ Temporary Admin Migration ============ */ + + /// @inheritdoc IEarnerManager + function migrate(address migrator_) external { + if (msg.sender != migrationAdmin) revert UnauthorizedMigration(); + + _migrate(migrator_); + } + + /* ============ View/Pure Functions ============ */ + + /// @inheritdoc IEarnerManager + function earnerStatusFor(address account_) external view returns (bool status_) { + return earnersListsIgnored() || isInRegistrarEarnersList(account_) || isInAdministratedEarnersList(account_); + } + + /// @inheritdoc IEarnerManager + function earnerStatusesFor(address[] calldata accounts_) external view returns (bool[] memory statuses_) { + statuses_ = new bool[](accounts_.length); + + bool earnersListsIgnored_ = earnersListsIgnored(); + + for (uint256 index_; index_ < accounts_.length; ++index_) { + if (earnersListsIgnored_) { + statuses_[index_] = true; + continue; + } + + address account_ = accounts_[index_]; + + if (isInRegistrarEarnersList(account_)) { + statuses_[index_] = true; + continue; + } + + statuses_[index_] = isInAdministratedEarnersList(account_); + } + } + + /// @inheritdoc IEarnerManager + function earnersListsIgnored() public view returns (bool isIgnored_) { + return IRegistrarLike(registrar).get(EARNERS_LIST_IGNORED_KEY) != bytes32(0); + } + + /// @inheritdoc IEarnerManager + function isInRegistrarEarnersList(address account_) public view returns (bool isInList_) { + return IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, account_); + } + + /// @inheritdoc IEarnerManager + function isInAdministratedEarnersList(address account_) public view returns (bool isInList_) { + return _isValidAdmin(_earnerDetails[account_].admin); + } + + /// @inheritdoc IEarnerManager + function getEarnerDetails(address account_) external view returns (bool status_, uint16 feeRate_, address admin_) { + if (earnersListsIgnored() || isInRegistrarEarnersList(account_)) return (true, 0, address(0)); + + EarnerDetails storage details_ = _earnerDetails[account_]; + + // NOTE: Not using `isInAdministratedEarnersList(account_)` here to avoid redundant storage reads. + return _isValidAdmin(details_.admin) ? (true, details_.feeRate, details_.admin) : (false, 0, address(0)); + } + + /// @inheritdoc IEarnerManager + function getEarnerDetails( + address[] calldata accounts_ + ) external view returns (bool[] memory statuses_, uint16[] memory feeRates_, address[] memory admins_) { + statuses_ = new bool[](accounts_.length); + feeRates_ = new uint16[](accounts_.length); + admins_ = new address[](accounts_.length); + + bool earnersListsIgnored_ = earnersListsIgnored(); + + for (uint256 index_; index_ < accounts_.length; ++index_) { + if (earnersListsIgnored_) { + statuses_[index_] = true; + continue; + } + + address account_ = accounts_[index_]; + + if (isInRegistrarEarnersList(account_)) { + statuses_[index_] = true; + continue; + } + + EarnerDetails storage details_ = _earnerDetails[account_]; + + // NOTE: Not using `isInAdministratedEarnersList(account_)` here to avoid redundant storage reads. + if (!_isValidAdmin(details_.admin)) continue; + + statuses_[index_] = true; + feeRates_[index_] = details_.feeRate; + admins_[index_] = details_.admin; + } + } + + /// @inheritdoc IEarnerManager + function isAdmin(address account_) public view returns (bool isAdmin_) { + // TODO: Consider transient storage for memoizing this check. + return IRegistrarLike(registrar).listContains(ADMINS_LIST_NAME, account_); + } + + /* ============ Internal Interactive Functions ============ */ + + /** + * @dev Sets the earner details for `account_`, assuming `msg.sender` is the calling admin. + * @param account_ The account under which yield could generate. + * @param status_ Whether the account is an earner, according to the admin. + * @param feeRate_ The fee rate to be taken from the yield. + */ + function _setDetails(address account_, bool status_, uint16 feeRate_) internal { + if (account_ == address(0)) revert ZeroAccount(); + if (!status_ && (feeRate_ != 0)) revert InvalidDetails(); // Fee rate must be zero if status is false. + if (feeRate_ > MAX_FEE_RATE) revert FeeRateTooHigh(); + if (isInRegistrarEarnersList(account_)) revert AlreadyInRegistrarEarnersList(account_); + + address admin_ = _earnerDetails[account_].admin; + + // Revert if the details have already been set by an admin that is not `msg.sender`, and is still an admin. + // NOTE: No `_isValidAdmin` here to avoid unnecessary contract call and storage reads if `admin_ == msg.sender`. + if ((admin_ != address(0)) && (admin_ != msg.sender) && isAdmin(admin_)) { + revert EarnerDetailsAlreadySet(account_); + } + + if (status_) { + _earnerDetails[account_] = EarnerDetails(msg.sender, feeRate_); + } else { + delete _earnerDetails[account_]; + } + + emit EarnerDetailsSet(account_, status_, msg.sender, feeRate_); + } + + /** + * @dev Reverts if the caller is not an admin. + */ + function _revertIfNotAdmin() internal view { + if (!isAdmin(msg.sender)) revert NotAdmin(); + } + + /* ============ Internal View/Pure Functions ============ */ + + /// @dev Returns the address of the contract to use as a migrator, if any. + function _getMigrator() internal view override returns (address migrator_) { + return + address( + uint160( + // NOTE: A subsequent implementation should use a unique migrator prefix. + uint256(IRegistrarLike(registrar).get(keccak256(abi.encode(MIGRATOR_KEY_PREFIX, address(this))))) + ) + ); + } + + /** + * @dev Returns whether `admin_` is a valid current admin. + * @param admin_ The admin to check. + * @return isValidAdmin_ True if `admin_` is a valid admin (non-zero and an admin according to the Registrar). + */ + function _isValidAdmin(address admin_) internal view returns (bool isValidAdmin_) { + return (admin_ != address(0)) && isAdmin(admin_); + } +} diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 60ab2ac..b3e1110 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -10,6 +10,7 @@ import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol"; import { ERC20Extended } from "../lib/common/src/ERC20Extended.sol"; import { Migratable } from "../lib/common/src/Migratable.sol"; +import { IEarnerManager } from "./interfaces/IEarnerManager.sol"; import { IMTokenLike } from "./interfaces/IMTokenLike.sol"; import { IRegistrarLike } from "./interfaces/IRegistrarLike.sol"; import { IWrappedMToken } from "./interfaces/IWrappedMToken.sol"; @@ -38,6 +39,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @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. + * @param hasEarnerDetails Whether the account has additional details for earning yield. */ struct Account { // First Slot @@ -46,10 +48,14 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { // Second slot uint112 earningPrincipal; bool hasClaimRecipient; + bool hasEarnerDetails; } /* ============ Variables ============ */ + /// @inheritdoc IWrappedMToken + uint16 public constant HUNDRED_PERCENT = 10_000; + /// @inheritdoc IWrappedMToken bytes32 public constant EARNERS_LIST_IGNORED_KEY = "earners_list_ignored"; @@ -62,6 +68,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken bytes32 public constant MIGRATOR_KEY_PREFIX = "wm_migrator_v2"; + /// @inheritdoc IWrappedMToken + address public immutable earnerManager; + /// @inheritdoc IWrappedMToken address public immutable migrationAdmin; @@ -101,17 +110,20 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * 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 earnerManager_ The address of an Earner Manager. * @param excessDestination_ The address of an excess destination. * @param migrationAdmin_ The address of a migration admin. */ constructor( address mToken_, address registrar_, + address earnerManager_, address excessDestination_, address migrationAdmin_ ) ERC20Extended("M (Wrapped) by M^0", "wM", 6) { if ((mToken = mToken_) == address(0)) revert ZeroMToken(); if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); + if ((earnerManager = earnerManager_) == address(0)) revert ZeroEarnerManager(); if ((excessDestination = excessDestination_) == address(0)) revert ZeroExcessDestination(); if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); } @@ -185,8 +197,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function enableEarning() external { - _revertIfNotApprovedEarner(address(this)); - + if (!_isThisApprovedEarner()) revert NotApprovedEarner(address(this)); if (isEarningEnabled()) revert EarningIsEnabled(); // NOTE: This is a temporary measure to prevent re-enabling earning after it has been disabled. @@ -204,8 +215,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function disableEarning() external { - _revertIfApprovedEarner(address(this)); - + if (_isThisApprovedEarner()) revert IsApprovedEarner(address(this)); if (!isEarningEnabled()) revert EarningIsDisabled(); uint128 currentMIndex_ = _currentMIndex(); @@ -225,11 +235,32 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { _startEarningFor(account_, _currentMIndex()); } + /// @inheritdoc IWrappedMToken + function startEarningFor(address[] calldata accounts_) external { + if (!isEarningEnabled()) revert EarningIsDisabled(); + + // NOTE: Use `currentIndex()` if/when upgrading to support `startEarningFor` while earning is disabled. + uint128 currentIndex_ = _currentMIndex(); + + for (uint256 index_; index_ < accounts_.length; ++index_) { + _startEarningFor(accounts_[index_], currentIndex_); + } + } + /// @inheritdoc IWrappedMToken function stopEarningFor(address account_) external { _stopEarningFor(account_, currentIndex()); } + /// @inheritdoc IWrappedMToken + function stopEarningFor(address[] calldata accounts_) external { + uint128 currentIndex_ = currentIndex(); + + for (uint256 index_; index_ < accounts_.length; ++index_) { + _stopEarningFor(accounts_[index_], currentIndex_); + } + } + /// @inheritdoc IWrappedMToken function setClaimRecipient(address claimRecipient_) external { _accounts[msg.sender].hasClaimRecipient = (_claimRecipients[msg.sender] = claimRecipient_) != address(0); @@ -484,9 +515,50 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { emit Claimed(account_, claimRecipient_, yield_); emit Transfer(address(0), account_, yield_); - if (claimRecipient_ == account_) return yield_; + uint240 yieldNetOfFees_ = yield_; + + if (accountInfo_.hasEarnerDetails) { + unchecked { + yieldNetOfFees_ -= _handleEarnerDetails(account_, yield_, currentIndex_); + } + } + + if ((claimRecipient_ == account_) || (yieldNetOfFees_ == 0)) return yield_; + + _transfer(account_, claimRecipient_, yieldNetOfFees_, currentIndex_); + } + + /** + * @dev Handles the computation and transfer of fees to the admin of an account with earner details. + * @param account_ The address of the account to handle earner details for. + * @param yield_ The yield accrued by the account. + * @param currentIndex_ The current index to use to compute the principal amount. + * @return fee_ The fee amount that was transferred to the admin. + */ + function _handleEarnerDetails( + address account_, + uint240 yield_, + uint128 currentIndex_ + ) internal returns (uint240 fee_) { + (, uint16 feeRate_, address admin_) = _getEarnerDetails(account_); + + if (admin_ == address(0)) { + // Prevent transferring to address(0) and remove `hasEarnerDetails` property going forward. + _accounts[account_].hasEarnerDetails = false; + return 0; + } + + if (feeRate_ == 0) return 0; + + feeRate_ = feeRate_ > HUNDRED_PERCENT ? HUNDRED_PERCENT : feeRate_; // Ensure fee rate is capped at 100%. + + unchecked { + fee_ = (feeRate_ * yield_) / HUNDRED_PERCENT; + } + + if (fee_ == 0) return 0; - _transfer(account_, claimRecipient_, yield_, currentIndex_); + _transfer(account_, admin_, fee_, currentIndex_); } /** @@ -644,7 +716,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param currentIndex_ The current index. */ function _startEarningFor(address account_, uint128 currentIndex_) internal { - _revertIfNotApprovedEarner(account_); + (bool isEarner_, , address admin_) = _getEarnerDetails(account_); + + if (!isEarner_) revert NotApprovedEarner(account_); Account storage accountInfo_ = _accounts[account_]; @@ -655,6 +729,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { accountInfo_.isEarning = true; accountInfo_.earningPrincipal = earningPrincipal_; + accountInfo_.hasEarnerDetails = admin_ != address(0); // Has earner details if an admin exists for this account. _addTotalEarningSupply(balance_, earningPrincipal_); @@ -671,7 +746,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param currentIndex_ The current index. */ function _stopEarningFor(address account_, uint128 currentIndex_) internal { - _revertIfApprovedEarner(account_); + (bool isEarner_, , ) = _getEarnerDetails(account_); + + if (isEarner_) revert IsApprovedEarner(account_); Account storage accountInfo_ = _accounts[account_]; @@ -684,6 +761,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { delete accountInfo_.isEarning; delete accountInfo_.earningPrincipal; + delete accountInfo_.hasEarnerDetails; _subtractTotalEarningSupply(balance_, earningPrincipal_); @@ -743,6 +821,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return IMTokenLike(mToken).currentIndex(); } + /// @dev Returns whether this contract is a Registrar-approved earner. + function _isThisApprovedEarner() internal view returns (bool) { + return + _getFromRegistrar(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || + IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, address(this)); + } + /// @dev Returns the earning index from the last `disableEarning` call. function _lastDisableEarningIndex() internal view returns (uint128 index_) { return wasEarningEnabled() ? _unsafeAccess(_enableDisableEarningIndices, 1) : 0; @@ -767,6 +852,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } } + /** + * @dev Retrieves the earner details for `account`. + * @param account_ The account being queried. + * @return isEarner_ Whether the account is an earner. + * @return feeRate_ The fee rate to be taken from the yield. + * @return admin_ The admin who set the details and who will collect the fee. + */ + function _getEarnerDetails( + address account_ + ) internal view returns (bool isEarner_, uint16 feeRate_, address admin_) { + return IEarnerManager(earnerManager).getEarnerDetails(account_); + } + /** * @dev Retrieve a value from the Registrar. * @param key_ The key to retrieve the value for. @@ -811,17 +909,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { ); } - /** - * @dev Returns whether `account_` is a Registrar-approved earner. - * @param account_ The account being queried. - * @return isApproved_ True if the account_ is a Registrar-approved earner, false otherwise. - */ - function _isApprovedEarner(address account_) internal view returns (bool isApproved_) { - return - _getFromRegistrar(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || - IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, account_); - } - /** * @dev Returns the M Token balance of `account_`. * @param account_ The account being queried. @@ -848,22 +935,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (account_ == address(0)) revert InvalidRecipient(account_); } - /** - * @dev Reverts if `account_` is an approved earner. - * @param account_ Address of an account. - */ - function _revertIfApprovedEarner(address account_) internal view { - if (_isApprovedEarner(account_)) revert IsApprovedEarner(account_); - } - - /** - * @dev Reverts if `account_` is not an approved earner. - * @param account_ Address of an account. - */ - function _revertIfNotApprovedEarner(address account_) internal view { - if (!_isApprovedEarner(account_)) revert NotApprovedEarner(account_); - } - /** * @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. diff --git a/src/MigratorV1.sol b/src/WrappedMTokenMigratorV1.sol similarity index 98% rename from src/MigratorV1.sol rename to src/WrappedMTokenMigratorV1.sol index 8eaef35..4dd3685 100644 --- a/src/MigratorV1.sol +++ b/src/WrappedMTokenMigratorV1.sol @@ -10,7 +10,7 @@ import { ListOfEarnersToMigrate } from "./ListOfEarnersToMigrate.sol"; * @title Migrator contract for migrating a WrappedMToken contract from V1 to V2. * @author M^0 Labs */ -contract MigratorV1 { +contract WrappedMTokenMigratorV1 { /* ============ Structs ============ */ /** diff --git a/src/interfaces/IEarnerManager.sol b/src/interfaces/IEarnerManager.sol new file mode 100644 index 0000000..1a02fa2 --- /dev/null +++ b/src/interfaces/IEarnerManager.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.26; + +import { IMigratable } from "../../lib/common/src/interfaces/IMigratable.sol"; + +/** + * @title Earner Status Manager interface for setting and returning earner status for Wrapped M Token accounts. + * @author M^0 Labs + */ +interface IEarnerManager is IMigratable { + /* ============ Events ============ */ + + /** + * @notice Emitted when the earner for `account` is set to `status`. + * @param account The account under which yield could generate. + * @param status Whether the account is set as an earner, according to the admin. + * @param admin The admin who set the details and who will collect the fee. + * @param feeRate The fee rate to be taken from the yield. + */ + event EarnerDetailsSet(address indexed account, bool indexed status, address indexed admin, uint16 feeRate); + + /* ============ Custom Errors ============ */ + + /// @notice Emitted when `account` is already in the earners list, so it cannot be added by an admin. + error AlreadyInRegistrarEarnersList(address account); + + /// @notice Emitted when the lengths of input arrays do not match. + error ArrayLengthMismatch(); + + /// @notice Emitted when the length of an input array is 0. + error ArrayLengthZero(); + + /// @notice Emitted when the earner details have already been set by an existing and active admin. + error EarnerDetailsAlreadySet(address account); + + /// @notice Emitted when the earners lists are ignored, thus not requiring admin to define earners. + error EarnersListsIgnored(); + + /// @notice Emitted when the fee rate provided is too high (higher than 100% in basis points). + error FeeRateTooHigh(); + + /// @notice Emitted when setting fee rate to a nonzero value while setting status to false. + error InvalidDetails(); + + /// @notice Emitted when the caller is not an admin. + error NotAdmin(); + + /// @notice Emitted when the non-governance migrate function is called by an account other than the migration admin. + error UnauthorizedMigration(); + + /// @notice Emitted when an account (whose status is being set) is 0x0. + error ZeroAccount(); + + /// @notice Emitted in constructor if Migration Admin is 0x0. + error ZeroMigrationAdmin(); + + /// @notice Emitted in constructor if Registrar is 0x0. + error ZeroRegistrar(); + + /* ============ Interactive Functions ============ */ + + /** + * @notice Sets the status for `account` to `status`. + * @notice If approving an earner that is already earning, but was recently removed from the Registrar earners list, + * call `wrappedM.stopEarning(account)` before calling this, then call `wrappedM.startEarning(account)`. + * @param account The account under which yield could generate. + * @param status Whether the account is an earner, according to the admin. + * @param feeRate The fee rate to be taken from the yield. + */ + function setEarnerDetails(address account, bool status, uint16 feeRate) external; + + /** + * @notice Sets the status for multiple accounts. + * @notice If approving an earner that is already earning, but was recently removed from the Registrar earners list, + * call `wrappedM.stopEarning(account)` before calling this, then call `wrappedM.startEarning(account)`. + * @param accounts The accounts under which yield could generate. + * @param statuses Whether each account is an earner, respectively, according to the admin. + * @param feeRates The fee rates to be taken from the yield, respectively. + */ + function setEarnerDetails( + address[] calldata accounts, + bool[] calldata statuses, + uint16[] calldata feeRates + ) external; + + /* ============ Temporary Admin Migration ============ */ + + /** + * @notice Performs an arbitrarily defined migration. + * @param migrator The address of a migrator contract. + */ + function migrate(address migrator) external; + + /* ============ View/Pure Functions ============ */ + + /// @notice Maximum fee rate that can be set (100% in basis points). + function MAX_FEE_RATE() external pure returns (uint16 maxFeeRate); + + /// @notice Registrar name of admins list. + function ADMINS_LIST_NAME() external pure returns (bytes32 adminsListName); + + /// @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 name of earners list. + function EARNERS_LIST_NAME() external pure returns (bytes32 earnersListName); + + /// @notice Registrar key prefix to determine the migrator contract. + function MIGRATOR_KEY_PREFIX() external pure returns (bytes32 migratorKeyPrefix); + + /** + * @notice Returns the earner status for `account`. + * @param account The account being queried. + * @return status Whether the account is an earner. + */ + function earnerStatusFor(address account) external view returns (bool status); + + /** + * @notice Returns the statuses for multiple accounts. + * @param accounts The accounts being queried. + * @return statuses Whether each account is an earner, respectively. + */ + function earnerStatusesFor(address[] calldata accounts) external view returns (bool[] memory statuses); + + /** + * @notice Returns whether the lists of earners can be ignored (thus making all accounts earners). + * @return ignored Whether the lists of earners can be ignored. + */ + function earnersListsIgnored() external view returns (bool ignored); + + /** + * @notice Returns whether `account` is a Registrar-approved earner. + * @param account The account being queried. + * @return isInList Whether the account is a Registrar-approved earner. + */ + function isInRegistrarEarnersList(address account) external view returns (bool isInList); + + /** + * @notice Returns whether `account` is an Admin-approved earner. + * @param account The account being queried. + * @return isInList Whether the account is an Admin-approved earner. + */ + function isInAdministratedEarnersList(address account) external view returns (bool isInList); + + /** + * @notice Returns the earner details for `account`. + * @param account The account being queried. + * @return status Whether the account is an earner. + * @return feeRate The fee rate to be taken from the yield. + * @return admin The admin who set the details and who will collect the fee. + */ + function getEarnerDetails(address account) external view returns (bool status, uint16 feeRate, address admin); + + /** + * @notice Returns the earner details for multiple accounts, according to an admin. + * @param accounts The accounts being queried. + * @return statuses Whether each account is an earner, respectively. + * @return feeRates The fee rates to be taken from the yield, respectively. + * @return admins The admin who set the details and who will collect the fee, respectively. + */ + function getEarnerDetails( + address[] calldata accounts + ) external view returns (bool[] memory statuses, uint16[] memory feeRates, address[] memory admins); + + /** + * @notice Returns whether `account` is an admin. + * @param account The address of an account. + * @return isAdmin Whether the account is an admin. + */ + function isAdmin(address account) external view returns (bool isAdmin); + + /// @notice The account that can bypass the Registrar and call the `migrate(address migrator)` function. + function migrationAdmin() external view returns (address migrationAdmin); + + /// @notice Returns the address of the Registrar. + function registrar() external view returns (address); +} diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 1f68e69..b3af5a3 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -69,7 +69,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { error EarningCannotBeReenabled(); /** - * @notice Emitted when calling `stopEarning` for an account approved as an earner by the Registrar. + * @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); @@ -83,7 +83,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { error InsufficientBalance(address account, uint240 balance, uint240 amount); /** - * @notice Emitted when calling `startEarning` for an account not approved as an earner by the Registrar. + * @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); @@ -91,9 +91,12 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted when there is no excess to claim. error NoExcess(); - /// @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 Earner Manager is 0x0. + error ZeroEarnerManager(); + /// @notice Emitted in constructor if Excess Destination is 0x0. error ZeroExcessDestination(); @@ -192,17 +195,29 @@ interface IWrappedMToken is IMigratable, IERC20Extended { function disableEarning() external; /** - * @notice Starts earning for `account` if allowed by the Registrar. + * @notice Starts earning for `account` if allowed by the Earner Manager. * @param account The account to start earning for. */ function startEarningFor(address account) external; /** - * @notice Stops earning for `account` if disallowed by the Registrar. + * @notice Starts earning for multiple accounts if individually allowed by the Earner Manager. + * @param accounts The accounts to start earning for. + */ + function startEarningFor(address[] calldata accounts) external; + + /** + * @notice Stops earning for `account` if disallowed by the Earner Manager. * @param account The account to stop earning for. */ function stopEarningFor(address account) external; + /** + * @notice Stops earning for multiple accounts if individually disallowed by the Earner Manager. + * @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. @@ -219,6 +234,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /* ============ View/Pure Functions ============ */ + /// @notice 100% in basis points. + function HUNDRED_PERCENT() external pure returns (uint16 hundredPercent); + /// @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); @@ -290,6 +308,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the Registrar. function registrar() external view returns (address registrar); + /// @notice The address of the Earner Manager. + function earnerManager() external view returns (address earnerManager); + /// @notice The portion of total supply that is not earning yield. function totalNonEarningSupply() external view returns (uint240 totalSupply); diff --git a/test/integration/Deploy.t.sol b/test/integration/Deploy.t.sol index ced7573..d8b3d57 100644 --- a/test/integration/Deploy.t.sol +++ b/test/integration/Deploy.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; import { Test } from "../../lib/forge-std/src/Test.sol"; +import { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { DeployBase } from "../../script/DeployBase.sol"; @@ -11,7 +12,8 @@ import { DeployBase } from "../../script/DeployBase.sol"; 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 _EARNER_MANAGER_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; @@ -20,25 +22,52 @@ contract DeployTests is Test, DeployBase { function test_deploy() external { vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); - (address expectedImplementation_, address expectedProxy_) = mockDeploy(_DEPLOYER, _DEPLOYER_NONCE); + ( + address expectedEarnerManagerImplementation_, + address expectedEarnerManagerProxy_, + address expectedWrappedMTokenImplementation_, + address expectedWrappedMTokenProxy_ + ) = mockDeploy(_DEPLOYER, _DEPLOYER_NONCE); vm.startPrank(_DEPLOYER); - (address implementation_, address proxy_) = deploy(_M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _MIGRATION_ADMIN); + ( + address earnerManagerImplementation_, + address earnerManagerProxy_, + address wrappedMTokenImplementation_, + address wrappedMTokenProxy_ + ) = deploy( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _WRAPPED_M_MIGRATION_ADMIN, + _EARNER_MANAGER_MIGRATION_ADMIN + ); vm.stopPrank(); + // Earner Manager Implementation assertions + assertEq(earnerManagerImplementation_, expectedEarnerManagerImplementation_); + assertEq(IEarnerManager(earnerManagerImplementation_).registrar(), _REGISTRAR); + + // Earner Manager Proxy assertions + assertEq(earnerManagerProxy_, expectedEarnerManagerProxy_); + assertEq(IEarnerManager(earnerManagerProxy_).registrar(), _REGISTRAR); + assertEq(IEarnerManager(earnerManagerProxy_).implementation(), earnerManagerImplementation_); + // Wrapped M Token Implementation assertions - assertEq(implementation_, expectedImplementation_); - assertEq(IWrappedMToken(implementation_).migrationAdmin(), _MIGRATION_ADMIN); - assertEq(IWrappedMToken(implementation_).mToken(), _M_TOKEN); - assertEq(IWrappedMToken(implementation_).registrar(), _REGISTRAR); - assertEq(IWrappedMToken(implementation_).excessDestination(), _EXCESS_DESTINATION); + assertEq(wrappedMTokenImplementation_, expectedWrappedMTokenImplementation_); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).earnerManager(), earnerManagerProxy_); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).excessDestination(), _EXCESS_DESTINATION); // Wrapped M Token Proxy assertions - assertEq(proxy_, expectedProxy_); - assertEq(IWrappedMToken(proxy_).migrationAdmin(), _MIGRATION_ADMIN); - assertEq(IWrappedMToken(proxy_).mToken(), _M_TOKEN); - assertEq(IWrappedMToken(proxy_).registrar(), _REGISTRAR); - assertEq(IWrappedMToken(proxy_).excessDestination(), _EXCESS_DESTINATION); - assertEq(IWrappedMToken(proxy_).implementation(), implementation_); + assertEq(wrappedMTokenProxy_, expectedWrappedMTokenProxy_); + assertEq(IWrappedMToken(wrappedMTokenProxy_).earnerManager(), earnerManagerProxy_); + assertEq(IWrappedMToken(wrappedMTokenProxy_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); + assertEq(IWrappedMToken(wrappedMTokenProxy_).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(wrappedMTokenProxy_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(wrappedMTokenProxy_).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(wrappedMTokenProxy_).implementation(), wrappedMTokenImplementation_); } } diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 5e5ce8c..3697d4c 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -7,11 +7,13 @@ import { IERC20Extended } from "../../lib/common/src/interfaces/IERC20Extended.s import { IERC712 } from "../../lib/common/src/interfaces/IERC712.sol"; import { Test } from "../../lib/forge-std/src/Test.sol"; +import { Proxy } from "../../lib/common/src/Proxy.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; +import { EarnerManager } from "../../src/EarnerManager.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; -import { MigratorV1 } from "../../src/MigratorV1.sol"; +import { WrappedMTokenMigratorV1 } from "../../src/WrappedMTokenMigratorV1.sol"; import { IMTokenLike, IRegistrarLike } from "./vendor/protocol/Interfaces.sol"; @@ -61,8 +63,10 @@ contract TestBase is Test { address[] internal _accounts = [_alice, _bob, _carol, _dave, _eric, _frank, _grace, _henry, _ivan, _judy]; - address internal _implementationV2; - address internal _migratorV1; + address internal _earnerManagerImplementation; + address internal _earnerManager; + address internal _wrappedMTokenImplementationV2; + address internal _wrappedMTokenMigratorV1; address[] internal _earners = [ 0x437cc33344a0B27A429f795ff6B469C72698B291, @@ -213,8 +217,10 @@ contract TestBase is Test { } function _deployV2Components() internal { - _implementationV2 = address( - new WrappedMToken(address(_mToken), _registrar, _excessDestination, _migrationAdmin) + _earnerManagerImplementation = address(new EarnerManager(_registrar, _migrationAdmin)); + _earnerManager = address(new Proxy(_earnerManagerImplementation)); + _wrappedMTokenImplementationV2 = address( + new WrappedMToken(address(_mToken), _registrar, _earnerManager, _excessDestination, _migrationAdmin) ); address[] memory earners_ = new address[](_earners.length); @@ -223,13 +229,13 @@ contract TestBase is Test { earners_[index_] = _earners[index_]; } - _migratorV1 = address(new MigratorV1(_implementationV2, earners_)); + _wrappedMTokenMigratorV1 = address(new WrappedMTokenMigratorV1(_wrappedMTokenImplementationV2, earners_)); } function _migrate() internal { _set( keccak256(abi.encode(_MIGRATOR_V1_PREFIX, address(_wrappedMToken))), - bytes32(uint256(uint160(_migratorV1))) + bytes32(uint256(uint160(_wrappedMTokenMigratorV1))) ); _wrappedMToken.migrate(); @@ -237,7 +243,7 @@ contract TestBase is Test { function _migrateFromAdmin() internal { vm.prank(_migrationAdmin); - _wrappedMToken.migrate(_migratorV1); + _wrappedMToken.migrate(_wrappedMTokenMigratorV1); } /* ============ utils ============ */ diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 9e066fe..7f5a663 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; import { Test } from "../../lib/forge-std/src/Test.sol"; +import { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { DeployBase } from "../../script/DeployBase.sol"; @@ -12,7 +13,8 @@ contract UpgradeTests is Test, DeployBase { address internal constant _WRAPPED_M_TOKEN = 0x437cc33344a0B27A429f795ff6B469C72698B291; 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 _EARNER_MANAGER_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; @@ -59,7 +61,12 @@ contract UpgradeTests is Test, DeployBase { function test_upgrade() external { vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); - (address expectedImplementation_, address expectedMigrator_) = mockDeployUpgrade(_DEPLOYER, _DEPLOYER_NONCE); + ( + address expectedEarnerManagerImplementation_, + address expectedEarnerManagerProxy_, + address expectedWrappedMTokenImplementation_, + address expectedWrappedMTokenMigrator_ + ) = mockDeployUpgrade(_DEPLOYER, _DEPLOYER_NONCE); address[] memory earners_ = new address[](_earners.length); @@ -68,36 +75,53 @@ contract UpgradeTests is Test, DeployBase { } vm.startPrank(_DEPLOYER); - (address implementation_, address migrator_) = deployUpgrade( - _M_TOKEN, - _REGISTRAR, - _EXCESS_DESTINATION, - _MIGRATION_ADMIN, - earners_ - ); + ( + address earnerManagerImplementation_, + address earnerManagerProxy_, + address wrappedMTokenImplementation_, + address wrappedMTokenMigrator_ + ) = deployUpgrade( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _WRAPPED_M_MIGRATION_ADMIN, + _EARNER_MANAGER_MIGRATION_ADMIN, + earners_ + ); vm.stopPrank(); + // Earner Manager Implementation assertions + assertEq(earnerManagerImplementation_, expectedEarnerManagerImplementation_); + assertEq(IEarnerManager(earnerManagerImplementation_).registrar(), _REGISTRAR); + + // Earner Manager Proxy assertions + assertEq(earnerManagerProxy_, expectedEarnerManagerProxy_); + assertEq(IEarnerManager(earnerManagerProxy_).registrar(), _REGISTRAR); + assertEq(IEarnerManager(earnerManagerProxy_).implementation(), earnerManagerImplementation_); + // Wrapped M Token Implementation assertions - assertEq(implementation_, expectedImplementation_); - assertEq(IWrappedMToken(implementation_).migrationAdmin(), _MIGRATION_ADMIN); - assertEq(IWrappedMToken(implementation_).mToken(), _M_TOKEN); - assertEq(IWrappedMToken(implementation_).registrar(), _REGISTRAR); - assertEq(IWrappedMToken(implementation_).excessDestination(), _EXCESS_DESTINATION); + assertEq(wrappedMTokenImplementation_, expectedWrappedMTokenImplementation_); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).earnerManager(), earnerManagerProxy_); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).excessDestination(), _EXCESS_DESTINATION); // Migrator assertions - assertEq(migrator_, expectedMigrator_); + assertEq(wrappedMTokenMigrator_, expectedWrappedMTokenMigrator_); uint240 totalEarningSupply_ = IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(); vm.prank(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin()); - IWrappedMToken(_WRAPPED_M_TOKEN).migrate(migrator_); + IWrappedMToken(_WRAPPED_M_TOKEN).migrate(wrappedMTokenMigrator_); // Wrapped M Token Proxy assertions - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin(), _MIGRATION_ADMIN); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).earnerManager(), earnerManagerProxy_); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).mToken(), _M_TOKEN); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).registrar(), _REGISTRAR); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).excessDestination(), _EXCESS_DESTINATION); - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).implementation(), implementation_); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).implementation(), wrappedMTokenImplementation_); // Relevant storage slots. assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(), totalEarningSupply_); diff --git a/test/unit/EarnerManager.sol b/test/unit/EarnerManager.sol new file mode 100644 index 0000000..39c3e7d --- /dev/null +++ b/test/unit/EarnerManager.sol @@ -0,0 +1,598 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +import { Test } from "../../lib/forge-std/src/Test.sol"; + +import { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol"; + +import { MockRegistrar } from "./../utils/Mocks.sol"; +import { EarnerManagerHarness } from "../utils/EarnerManagerHarness.sol"; + +contract EarnerManagerTests is Test { + bytes32 internal constant _EARNERS_LIST_IGNORED_KEY = "earners_list_ignored"; + bytes32 internal constant _EARNERS_LIST_NAME = "earners"; + bytes32 internal constant _ADMINS_LIST_NAME = "em_admins"; + + address internal _admin1 = makeAddr("admin1"); + address internal _admin2 = makeAddr("admin2"); + + address internal _alice = makeAddr("alice"); + address internal _bob = makeAddr("bob"); + address internal _carol = makeAddr("carol"); + address internal _dave = makeAddr("dave"); + address internal _frank = makeAddr("frank"); + + address internal _migrationAdmin = makeAddr("migrationAdmin"); + + MockRegistrar internal _registrar; + EarnerManagerHarness internal _earnerManager; + + function setUp() external { + _registrar = new MockRegistrar(); + _earnerManager = new EarnerManagerHarness(address(_registrar), _migrationAdmin); + + _registrar.setListContains(_ADMINS_LIST_NAME, _admin1, true); + _registrar.setListContains(_ADMINS_LIST_NAME, _admin2, true); + } + + /* ============ initial state ============ */ + function test_initialState() external view { + assertEq(_earnerManager.registrar(), address(_registrar)); + } + + /* ============ constructor ============ */ + function test_constructor_zeroRegistrar() external { + vm.expectRevert(IEarnerManager.ZeroRegistrar.selector); + new EarnerManagerHarness(address(0), address(0)); + } + + function test_constructor_zeroMigrationAdmin() external { + vm.expectRevert(IEarnerManager.ZeroMigrationAdmin.selector); + new EarnerManagerHarness(address(_registrar), address(0)); + } + + /* ============ _setDetails ============ */ + function test_setDetails_zeroAccount() external { + vm.expectRevert(IEarnerManager.ZeroAccount.selector); + + _earnerManager.setDetails(address(0), false, 0); + } + + function test_setDetails_invalidDetails() external { + vm.expectRevert(IEarnerManager.InvalidDetails.selector); + + _earnerManager.setDetails(_alice, false, 1); + } + + function test_setDetails_feeRateTooHigh() external { + vm.expectRevert(IEarnerManager.FeeRateTooHigh.selector); + + _earnerManager.setDetails(_alice, true, 10_001); + } + + function test_setDetails_alreadyInRegistrarEarnersList() external { + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + vm.expectRevert(abi.encodeWithSelector(IEarnerManager.AlreadyInRegistrarEarnersList.selector, _alice)); + + _earnerManager.setDetails(_alice, true, 0); + } + + function test_setDetails_earnerDetailsAlreadySet() external { + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); + + vm.expectRevert(abi.encodeWithSelector(IEarnerManager.EarnerDetailsAlreadySet.selector, _alice)); + + vm.prank(_admin2); + _earnerManager.setDetails(_alice, true, 2); + } + + function test_setDetails() external { + vm.prank(_admin1); + _earnerManager.setDetails(_alice, true, 1); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 1); + assertEq(admin_, _admin1); + } + + function test_setDetails_changeFeeRate() external { + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 1); + assertEq(admin_, _admin1); + + vm.prank(_admin1); + _earnerManager.setDetails(_alice, true, 2); + + (status_, feeRate_, admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 2); + assertEq(admin_, _admin1); + } + + function test_setDetails_remove() external { + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 1); + assertEq(admin_, _admin1); + + vm.prank(_admin1); + _earnerManager.setDetails(_alice, false, 0); + + (status_, feeRate_, admin_) = _earnerManager.getEarnerDetails(_alice); + + assertFalse(status_); + assertEq(feeRate_, 0); + assertEq(admin_, address(0)); + } + + /* ============ setEarnerDetails ============ */ + function test_setEarnerDetails_notAdmin() external { + vm.expectRevert(IEarnerManager.NotAdmin.selector); + + vm.prank(_bob); + _earnerManager.setEarnerDetails(_alice, true, 0); + } + + function test_setEarnerDetails_earnersListIgnored() external { + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + + vm.expectRevert(IEarnerManager.EarnersListsIgnored.selector); + + vm.prank(_admin1); + _earnerManager.setEarnerDetails(_alice, true, 0); + } + + function test_setEarnerDetails() external { + vm.expectEmit(); + emit IEarnerManager.EarnerDetailsSet(_alice, true, _admin1, 10_000); + + vm.prank(_admin1); + _earnerManager.setEarnerDetails(_alice, true, 10_000); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 10_000); + assertEq(admin_, _admin1); + } + + /* ============ setEarnerDetails batch ============ */ + function test_setEarnerDetails_batch_notAdmin() external { + vm.expectRevert(IEarnerManager.NotAdmin.selector); + + vm.prank(_alice); + _earnerManager.setEarnerDetails(new address[](0), new bool[](0), new uint16[](0)); + } + + function test_setEarnerDetails_batch_arrayLengthZero() external { + vm.expectRevert(IEarnerManager.ArrayLengthZero.selector); + + vm.prank(_admin1); + _earnerManager.setEarnerDetails(new address[](0), new bool[](2), new uint16[](2)); + } + + function test_setEarnerDetails_batch_arrayLengthMismatch() external { + vm.expectRevert(IEarnerManager.ArrayLengthMismatch.selector); + + vm.prank(_admin1); + _earnerManager.setEarnerDetails(new address[](1), new bool[](2), new uint16[](2)); + + vm.expectRevert(IEarnerManager.ArrayLengthMismatch.selector); + + vm.prank(_admin1); + _earnerManager.setEarnerDetails(new address[](2), new bool[](1), new uint16[](2)); + + vm.expectRevert(IEarnerManager.ArrayLengthMismatch.selector); + + vm.prank(_admin1); + _earnerManager.setEarnerDetails(new address[](2), new bool[](2), new uint16[](1)); + } + + function test_setEarnerDetails_batch_earnersListIgnored() external { + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + + vm.expectRevert(IEarnerManager.EarnersListsIgnored.selector); + + vm.prank(_admin1); + _earnerManager.setEarnerDetails(new address[](2), new bool[](2), new uint16[](2)); + } + + function test_setEarnerDetails_batch() external { + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + bool[] memory statuses_ = new bool[](2); + statuses_[0] = true; + statuses_[1] = true; + + uint16[] memory feeRates = new uint16[](2); + feeRates[0] = 1; + feeRates[1] = 10_000; + + vm.expectEmit(); + emit IEarnerManager.EarnerDetailsSet(_alice, true, _admin1, 1); + + vm.expectEmit(); + emit IEarnerManager.EarnerDetailsSet(_bob, true, _admin1, 10_000); + + vm.prank(_admin1); + _earnerManager.setEarnerDetails(accounts_, statuses_, feeRates); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 1); + assertEq(admin_, _admin1); + + (status_, feeRate_, admin_) = _earnerManager.getEarnerDetails(_bob); + + assertTrue(status_); + assertEq(feeRate_, 10_000); + assertEq(admin_, _admin1); + } + + /* ============ earnerStatusFor ============ */ + function test_earnerStatusFor_earnersListIgnored() external { + assertFalse(_earnerManager.earnerStatusFor(_alice)); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + + assertTrue(_earnerManager.earnerStatusFor(_alice)); + } + + function test_earnerStatusFor_inEarnersList() external { + assertFalse(_earnerManager.earnerStatusFor(_alice)); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + assertTrue(_earnerManager.earnerStatusFor(_alice)); + } + + function test_earnerStatusFor_setByAdmin() external { + assertFalse(_earnerManager.earnerStatusFor(_alice)); + + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); + + assertTrue(_earnerManager.earnerStatusFor(_alice)); + } + + function test_earnerStatusFor_earnersListIgnoredAndInEarnersList() external { + assertFalse(_earnerManager.earnerStatusFor(_alice)); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + assertTrue(_earnerManager.earnerStatusFor(_alice)); + } + + function test_earnerStatusFor_inEarnersListAndSetByAdmin() external { + assertFalse(_earnerManager.earnerStatusFor(_alice)); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); + + assertTrue(_earnerManager.earnerStatusFor(_alice)); + } + + function test_earnerStatusFor_earnersListIgnoredAndSetByAdmin() external { + assertFalse(_earnerManager.earnerStatusFor(_alice)); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); + + assertTrue(_earnerManager.earnerStatusFor(_alice)); + } + + function test_earnerStatusFor_earnersListIgnoredAndInEarnersListAndSetByAdmin() external { + assertFalse(_earnerManager.earnerStatusFor(_alice)); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); + + assertTrue(_earnerManager.earnerStatusFor(_alice)); + } + + /* ============ earnerStatusesFor ============ */ + function test_earnerStatusesFor_earnersListIgnored() external { + address[] memory accounts_ = new address[](3); + accounts_[0] = _alice; + accounts_[1] = _bob; + accounts_[2] = _carol; + + bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertFalse(statuses_[0]); + assertFalse(statuses_[1]); + assertFalse(statuses_[2]); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + + statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertTrue(statuses_[0]); + assertTrue(statuses_[1]); + assertTrue(statuses_[2]); + } + + function test_earnerStatusesFor_inEarnersList() external { + address[] memory accounts_ = new address[](3); + accounts_[0] = _alice; + accounts_[1] = _bob; + accounts_[2] = _carol; + + bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertFalse(statuses_[0]); + assertFalse(statuses_[1]); + assertFalse(statuses_[2]); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertTrue(statuses_[0]); + assertFalse(statuses_[1]); + assertFalse(statuses_[2]); + } + + function test_earnerStatusesFor_setByAdmin() external { + address[] memory accounts_ = new address[](3); + accounts_[0] = _alice; + accounts_[1] = _bob; + accounts_[2] = _carol; + + bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertFalse(statuses_[0]); + assertFalse(statuses_[1]); + assertFalse(statuses_[2]); + + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); + _earnerManager.setInternalEarnerDetails(_bob, _admin2, 0); + + statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertTrue(statuses_[0]); + assertTrue(statuses_[1]); + assertFalse(statuses_[2]); + } + + function test_earnerStatusesFor_earnersListIgnoredAndInEarnersList() external { + address[] memory accounts_ = new address[](3); + accounts_[0] = _alice; + accounts_[1] = _bob; + accounts_[2] = _carol; + + bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertFalse(statuses_[0]); + assertFalse(statuses_[1]); + assertFalse(statuses_[2]); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertTrue(statuses_[0]); + assertTrue(statuses_[1]); + assertTrue(statuses_[2]); + } + + function test_earnerStatusesFor_inEarnersListAndSetByAdmin() external { + address[] memory accounts_ = new address[](3); + accounts_[0] = _alice; + accounts_[1] = _bob; + accounts_[2] = _carol; + + bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertFalse(statuses_[0]); + assertFalse(statuses_[1]); + assertFalse(statuses_[2]); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); + _earnerManager.setInternalEarnerDetails(_bob, _admin1, 0); + _earnerManager.setInternalEarnerDetails(_carol, _admin2, 0); + + statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertTrue(statuses_[0]); + assertTrue(statuses_[1]); + assertTrue(statuses_[2]); + } + + function test_earnerStatusesFor_earnersListIgnoredAndSetByAdmin() external { + address[] memory accounts_ = new address[](3); + accounts_[0] = _alice; + accounts_[1] = _bob; + accounts_[2] = _carol; + + bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertFalse(statuses_[0]); + assertFalse(statuses_[1]); + assertFalse(statuses_[2]); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); + _earnerManager.setInternalEarnerDetails(_bob, _admin2, 0); + + statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertTrue(statuses_[0]); + assertTrue(statuses_[1]); + assertTrue(statuses_[2]); + } + + function test_earnerStatusesFor_earnersListIgnoredAndInEarnersListAndSetByAdmin() external { + address[] memory accounts_ = new address[](3); + accounts_[0] = _alice; + accounts_[1] = _bob; + accounts_[2] = _carol; + + bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertFalse(statuses_[0]); + assertFalse(statuses_[1]); + assertFalse(statuses_[2]); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); + _earnerManager.setInternalEarnerDetails(_bob, _admin1, 0); + _earnerManager.setInternalEarnerDetails(_carol, _admin2, 0); + + statuses_ = _earnerManager.earnerStatusesFor(accounts_); + + assertTrue(statuses_[0]); + assertTrue(statuses_[1]); + assertTrue(statuses_[2]); + } + + /* ============ earnersListsIgnored ============ */ + function test_earnersListsIgnored() external { + assertFalse(_earnerManager.earnersListsIgnored()); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + + assertTrue(_earnerManager.earnersListsIgnored()); + } + + /* ============ isInRegistrarEarnersList ============ */ + function test_isInRegistrarEarnersList() external { + assertFalse(_earnerManager.isInRegistrarEarnersList(_alice)); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + assertTrue(_earnerManager.isInRegistrarEarnersList(_alice)); + } + + /* ============ isInAdministratedEarnersList ============ */ + function test_isInAdministratedEarnersList() external { + assertFalse(_earnerManager.isInAdministratedEarnersList(_alice)); + + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); + + assertTrue(_earnerManager.isInAdministratedEarnersList(_alice)); + } + + /* ============ getEarnerDetails ============ */ + function test_getEarnerDetails_earnersListIgnored() external { + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); + + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 0); + assertEq(admin_, address(0)); + } + + function test_getEarnerDetails_inEarnersList() external { + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); + + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 0); + assertEq(admin_, address(0)); + } + + function test_getEarnerDetails_invalidAdmin() external { + _earnerManager.setInternalEarnerDetails(_alice, _bob, 1); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertFalse(status_); + assertEq(feeRate_, 0); + assertEq(admin_, address(0)); + } + + function test_getEarnerDetails() external { + _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); + + (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); + + assertTrue(status_); + assertEq(feeRate_, 1); + assertEq(admin_, _admin1); + } + + /* ============ getEarnerDetails batch ============ */ + function test_getEarnerDetails_batch_earnersListIgnored() external { + _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); + + address[] memory accounts_ = new address[](2); + accounts_[0] = _alice; + accounts_[1] = _bob; + + (bool[] memory statuses_, uint16[] memory feeRates_, address[] memory admins_) = _earnerManager + .getEarnerDetails(accounts_); + + assertTrue(statuses_[0]); + assertEq(feeRates_[0], 0); + assertEq(admins_[0], address(0)); + + assertTrue(statuses_[1]); + assertEq(feeRates_[1], 0); + assertEq(admins_[1], address(0)); + } + + function test_getEarnerDetails_batch() external { + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + + _earnerManager.setInternalEarnerDetails(_bob, _admin1, 1); + _earnerManager.setInternalEarnerDetails(_carol, _frank, 2); // Invalid admin + + address[] memory accounts_ = new address[](4); + accounts_[0] = _alice; + accounts_[1] = _bob; + accounts_[2] = _carol; + accounts_[3] = _dave; + + (bool[] memory statuses_, uint16[] memory feeRates_, address[] memory admins_) = _earnerManager + .getEarnerDetails(accounts_); + + assertTrue(statuses_[0]); + assertEq(feeRates_[0], 0); + assertEq(admins_[0], address(0)); + + assertTrue(statuses_[1]); + assertEq(feeRates_[1], 1); + assertEq(admins_[1], _admin1); + + assertFalse(statuses_[2]); + assertEq(feeRates_[2], 0); + assertEq(admins_[2], address(0)); + + assertFalse(statuses_[3]); + assertEq(feeRates_[3], 0); + assertEq(admins_[3], address(0)); + } + + /* ============ isAdmin ============ */ + function test_isAdmin() external view { + assertFalse(_earnerManager.isAdmin(_alice)); + assertTrue(_earnerManager.isAdmin(_admin1)); + assertTrue(_earnerManager.isAdmin(_admin2)); + } +} diff --git a/test/unit/Migration.t.sol b/test/unit/Migration.t.sol deleted file mode 100644 index ea1a84f..0000000 --- a/test/unit/Migration.t.sol +++ /dev/null @@ -1,75 +0,0 @@ -// 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 { MigratorV1 as Migrator } from "../../src/MigratorV1.sol"; - -import { MockRegistrar } from "./../utils/Mocks.sol"; - -contract WrappedMTokenV3 { - function foo() external pure returns (uint256) { - return 1; - } -} - -contract MigrationTests is Test { - bytes32 internal constant _MIGRATOR_KEY_PREFIX = "wm_migrator_v2"; - - 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 _excessDestination = makeAddr("excessDestination"); - address internal _migrationAdmin = makeAddr("migrationAdmin"); - - MockRegistrar internal _registrar; - WrappedMToken internal _implementation; - IWrappedMToken internal _wrappedMToken; - - function setUp() external { - _registrar = new MockRegistrar(); - - _implementation = new WrappedMToken(_mToken, address(_registrar), _excessDestination, _migrationAdmin); - - _wrappedMToken = IWrappedMToken(address(new Proxy(address(_implementation)))); - } - - function test_migration() external { - WrappedMTokenV3 implementationV3_ = new WrappedMTokenV3(); - address migrator_ = address(new Migrator(address(implementationV3_), new address[](0))); - - _registrar.set( - keccak256(abi.encode(_MIGRATOR_KEY_PREFIX, address(_wrappedMToken))), - bytes32(uint256(uint160(migrator_))) - ); - - vm.expectRevert(); - WrappedMTokenV3(address(_wrappedMToken)).foo(); - - _wrappedMToken.migrate(); - - assertEq(WrappedMTokenV3(address(_wrappedMToken)).foo(), 1); - } - - function test_migration_fromAdmin() external { - WrappedMTokenV3 implementationV3_ = new WrappedMTokenV3(); - address migrator_ = address(new Migrator(address(implementationV3_), new address[](0))); - - vm.expectRevert(); - WrappedMTokenV3(address(_wrappedMToken)).foo(); - - vm.prank(_migrationAdmin); - _wrappedMToken.migrate(migrator_); - - assertEq(WrappedMTokenV3(address(_wrappedMToken)).foo(), 1); - } -} diff --git a/test/unit/Migrations.t.sol b/test/unit/Migrations.t.sol new file mode 100644 index 0000000..b58c89e --- /dev/null +++ b/test/unit/Migrations.t.sol @@ -0,0 +1,140 @@ +// 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 { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol"; +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; + +import { EarnerManager } from "../../src/EarnerManager.sol"; +import { WrappedMToken } from "../../src/WrappedMToken.sol"; +import { WrappedMTokenMigratorV1 as WrappedMTokenMigrator } from "../../src/WrappedMTokenMigratorV1.sol"; + +import { MockRegistrar } from "./../utils/Mocks.sol"; + +contract Foo { + function bar() external pure returns (uint256) { + return 1; + } +} + +contract EarnerManagerMigrator { + uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + address public immutable implementationV2; + + constructor(address implementation_) { + implementationV2 = implementation_; + } + + fallback() external virtual { + address implementation_ = implementationV2; + + assembly { + sstore(_IMPLEMENTATION_SLOT, implementation_) + } + } +} + +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 _earnerManager = makeAddr("earnerManager"); + address internal _excessDestination = makeAddr("excessDestination"); + address internal _migrationAdmin = makeAddr("migrationAdmin"); + + function test_wrappedMToken_migration() external { + MockRegistrar registrar_ = new MockRegistrar(); + address mToken_ = makeAddr("mToken"); + + address implementation_ = address( + new WrappedMToken( + address(mToken_), + address(registrar_), + _earnerManager, + _excessDestination, + _migrationAdmin + ) + ); + + address proxy_ = address(new Proxy(address(implementation_))); + address migrator_ = address(new WrappedMTokenMigrator(address(new Foo()), new address[](0))); + + 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); + } + + function test_wrappedMToken_migration_fromAdmin() external { + MockRegistrar registrar_ = new MockRegistrar(); + address mToken_ = makeAddr("mToken"); + + address implementation_ = address( + new WrappedMToken( + address(mToken_), + address(registrar_), + _earnerManager, + _excessDestination, + _migrationAdmin + ) + ); + + address proxy_ = address(new Proxy(address(implementation_))); + address migrator_ = address(new WrappedMTokenMigrator(address(new Foo()), new address[](0))); + + vm.expectRevert(); + Foo(proxy_).bar(); + + vm.prank(_migrationAdmin); + IWrappedMToken(proxy_).migrate(migrator_); + + assertEq(Foo(proxy_).bar(), 1); + } + + function test_earnerManager_migration() external { + MockRegistrar registrar_ = new MockRegistrar(); + + address implementation_ = address(new EarnerManager(address(registrar_), _migrationAdmin)); + address proxy_ = address(new Proxy(address(implementation_))); + address migrator_ = address(new EarnerManagerMigrator(address(new Foo()))); + + registrar_.set(keccak256(abi.encode(_EM_MIGRATOR_KEY_PREFIX, proxy_)), bytes32(uint256(uint160(migrator_)))); + + vm.expectRevert(); + Foo(proxy_).bar(); + + IWrappedMToken(proxy_).migrate(); + + assertEq(Foo(proxy_).bar(), 1); + } + + function test_earnerManager_migration_fromAdmin() external { + MockRegistrar registrar_ = new MockRegistrar(); + + address implementation_ = address(new EarnerManager(address(registrar_), _migrationAdmin)); + address proxy_ = address(new Proxy(address(implementation_))); + address migrator_ = address(new EarnerManagerMigrator(address(new Foo()))); + + vm.expectRevert(); + Foo(proxy_).bar(); + + vm.prank(_migrationAdmin); + IEarnerManager(proxy_).migrate(migrator_); + + assertEq(Foo(proxy_).bar(), 1); + } +} diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index d4d3b5c..75d4a63 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -11,7 +11,7 @@ import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; -import { MockM, MockRegistrar } from "../utils/Mocks.sol"; +import { MockEarnerManager, MockM, MockRegistrar } from "../utils/Mocks.sol"; contract StoryTests is Test { uint56 internal constant _EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; @@ -26,6 +26,7 @@ contract StoryTests is Test { address internal _excessDestination = makeAddr("excessDestination"); address internal _migrationAdmin = makeAddr("migrationAdmin"); + MockEarnerManager internal _earnerManager; MockM internal _mToken; MockRegistrar internal _registrar; WrappedMToken internal _implementation; @@ -37,14 +38,22 @@ contract StoryTests is Test { _mToken = new MockM(); _mToken.setCurrentIndex(_EXP_SCALED_ONE); - _implementation = new WrappedMToken(address(_mToken), address(_registrar), _excessDestination, _migrationAdmin); + _earnerManager = new MockEarnerManager(); + + _implementation = new WrappedMToken( + address(_mToken), + address(_registrar), + address(_earnerManager), + _excessDestination, + _migrationAdmin + ); _wrappedMToken = IWrappedMToken(address(new Proxy(address(_implementation)))); } function test_story() external { - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -241,7 +250,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 283_333328); assertEq(_wrappedMToken.excess(), 416_666672); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, false); + _earnerManager.setEarnerDetails(_alice, false, 0, address(0)); _wrappedMToken.stopEarningFor(_alice); @@ -256,7 +265,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 116_666664); assertEq(_wrappedMToken.excess(), 416_666672); - _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); + _earnerManager.setEarnerDetails(_carol, true, 0, address(0)); _wrappedMToken.startEarningFor(_carol); @@ -355,8 +364,8 @@ contract StoryTests is Test { } function test_noExcessCreep() external { - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _mToken.setCurrentIndex(_EXP_SCALED_ONE + 3e11 - 1); @@ -393,8 +402,8 @@ contract StoryTests is Test { } function test_dustWrapping() external { - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _mToken.setCurrentIndex(_EXP_SCALED_ONE + 1); diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 7035589..44c3603 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -13,16 +13,17 @@ import { Test } from "../../lib/forge-std/src/Test.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { MockM, MockRegistrar } from "../utils/Mocks.sol"; +import { MockEarnerManager, MockM, MockRegistrar } from "../utils/Mocks.sol"; import { WrappedMTokenHarness } from "../utils/WrappedMTokenHarness.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. -// TODO: Remove unneeded _wrappedMToken.enableEarning. contract WrappedMTokenTests 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 _EARNERS_LIST_NAME = "earners"; @@ -37,6 +38,7 @@ contract WrappedMTokenTests is Test { address[] internal _accounts = [_alice, _bob, _charlie, _david]; + MockEarnerManager internal _earnerManager; MockM internal _mToken; MockRegistrar internal _registrar; WrappedMTokenHarness internal _implementation; @@ -47,9 +49,12 @@ contract WrappedMTokenTests is Test { _mToken = new MockM(); + _earnerManager = new MockEarnerManager(); + _implementation = new WrappedMTokenHarness( address(_mToken), address(_registrar), + address(_earnerManager), _excessDestination, _migrationAdmin ); @@ -79,22 +84,39 @@ contract WrappedMTokenTests is Test { function test_constructor_zeroMToken() external { vm.expectRevert(IWrappedMToken.ZeroMToken.selector); - new WrappedMTokenHarness(address(0), address(0), address(0), address(0)); + new WrappedMTokenHarness(address(0), 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)); + new WrappedMTokenHarness(address(_mToken), address(0), address(0), address(0), address(0)); + } + + function test_constructor_zeroEarnerManager() external { + vm.expectRevert(IWrappedMToken.ZeroEarnerManager.selector); + new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0), address(0)); } function test_constructor_zeroExcessDestination() external { vm.expectRevert(IWrappedMToken.ZeroExcessDestination.selector); - new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0)); + new WrappedMTokenHarness( + address(_mToken), + address(_registrar), + address(_earnerManager), + address(0), + address(0) + ); } function test_constructor_zeroMigrationAdmin() external { vm.expectRevert(IWrappedMToken.ZeroMigrationAdmin.selector); - new WrappedMTokenHarness(address(_mToken), address(_registrar), _excessDestination, address(0)); + new WrappedMTokenHarness( + address(_mToken), + address(_registrar), + address(_earnerManager), + _excessDestination, + address(0) + ); } function test_constructor_zeroImplementation() external { @@ -155,7 +177,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -434,7 +456,7 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_100000000000); _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setAccountOf(_alice, 999, 909, false); + _wrappedMToken.setAccountOf(_alice, 999, 909, false, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); _wrappedMToken.internalUnwrap(_alice, _alice, 1_000); @@ -509,7 +531,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -672,7 +694,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -705,7 +727,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -733,13 +755,95 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 0); } + function test_claimFor_earner_withFee() external { + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, true); + _earnerManager.setEarnerDetails(_alice, true, 1_500, _bob); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 15); + + assertEq(_wrappedMToken.claimFor(_alice), 100); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_085); + assertEq(_wrappedMToken.balanceOf(_bob), 15); + } + + function test_claimFor_earner_withFeeAboveOneHundredPercent() external { + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, true); + _earnerManager.setEarnerDetails(_alice, true, type(uint16).max, _bob); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 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.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_bob), 100); + } + + function test_claimFor_earner_withOverrideRecipientAndFee() external { + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_charlie))) + ); + + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, true); + _earnerManager.setEarnerDetails(_alice, true, 1_500, _bob); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _charlie, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, 15); + + vm.expectEmit(); + emit IERC20.Transfer(_alice, _charlie, 85); + + assertEq(_wrappedMToken.claimFor(_alice), 100); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.balanceOf(_bob), 15); + assertEq(_wrappedMToken.balanceOf(_charlie), 85); + } + function testFuzz_claimFor( bool earningEnabled_, bool accountEarning_, uint240 balanceWithYield_, uint240 balance_, uint128 currentMIndex_, - bool claimOverride_ + bool claimOverride_, + uint16 feeRate_ ) external { currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); @@ -753,10 +857,15 @@ contract WrappedMTokenTests is Test { _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); + if (feeRate_ != 0) { + _wrappedMToken.setHasEarnerDetails(_alice, true); + _earnerManager.setEarnerDetails(_alice, true, feeRate_, _bob); + } + if (claimOverride_) { _registrar.set( keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), - bytes32(uint256(uint160(_bob))) + bytes32(uint256(uint160(_charlie))) ); } @@ -764,15 +873,31 @@ contract WrappedMTokenTests is Test { if (accruedYield_ != 0) { vm.expectEmit(); - emit IWrappedMToken.Claimed(_alice, claimOverride_ ? _bob : _alice, accruedYield_); + emit IWrappedMToken.Claimed(_alice, claimOverride_ ? _charlie : _alice, accruedYield_); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, accruedYield_); } + uint240 fee_ = (accruedYield_ * (feeRate_ > _ONE_HUNDRED_PERCENT ? _ONE_HUNDRED_PERCENT : feeRate_)) / + _ONE_HUNDRED_PERCENT; + + if (fee_ != 0) { + vm.expectEmit(); + emit IERC20.Transfer(_alice, _bob, fee_); + } + + if (claimOverride_ && (accruedYield_ - fee_ != 0)) { + vm.expectEmit(); + emit IERC20.Transfer(_alice, _charlie, accruedYield_ - fee_); + } + assertEq(_wrappedMToken.claimFor(_alice), accruedYield_); - assertEq(_wrappedMToken.totalSupply(), _wrappedMToken.balanceOf(_alice) + _wrappedMToken.balanceOf(_bob)); + assertEq( + _wrappedMToken.totalSupply(), + _wrappedMToken.balanceOf(_alice) + _wrappedMToken.balanceOf(_bob) + _wrappedMToken.balanceOf(_charlie) + ); } /* ============ claimExcess ============ */ @@ -864,7 +989,7 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_100000000000); _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 1_000, 1_001)); vm.prank(_alice); @@ -930,7 +1055,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalNonEarningSupply(500); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. _wrappedMToken.setAccountOf(_bob, 500); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -980,7 +1105,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalNonEarningSupply(1_000); _wrappedMToken.setAccountOf(_alice, 1_000); - _wrappedMToken.setAccountOf(_bob, 500, 500, false); // 550 balance with yield. + _wrappedMToken.setAccountOf(_bob, 500, 500, false, false); // 550 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); @@ -1009,8 +1134,8 @@ contract WrappedMTokenTests is Test { _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. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_bob, 500, 500, false, false); // 550 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); @@ -1058,7 +1183,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -1164,7 +1289,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, aliceBalance_); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); vm.expectRevert(UIntMath.InvalidUInt112.selector); _wrappedMToken.startEarningFor(_alice); @@ -1178,7 +1303,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); vm.expectEmit(); emit IWrappedMToken.StartedEarning(_alice); @@ -1205,7 +1330,7 @@ contract WrappedMTokenTests is Test { _setupAccount(_alice, false, balance_, balance_); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); if (earningEnabled_) { vm.expectEmit(); @@ -1229,9 +1354,51 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningPrincipal(), earningPrincipal_); } + /* ============ startEarningFor batch ============ */ + 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_100000000000); + + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + + 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() external { + _mToken.setCurrentIndex(1_100000000000); + + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); + + 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); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.IsApprovedEarner.selector, _alice)); _wrappedMToken.stopEarningFor(_alice); @@ -1244,7 +1411,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -1316,7 +1483,7 @@ contract WrappedMTokenTests is Test { /* ============ setClaimRecipient ============ */ function test_setClaimRecipient() external { - (, , , bool hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); + (, , , bool hasClaimRecipient_, ) = _wrappedMToken.getAccountOf(_alice); assertFalse(hasClaimRecipient_); assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), address(0)); @@ -1324,7 +1491,7 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.setClaimRecipient(_alice); - (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); + (, , , hasClaimRecipient_, ) = _wrappedMToken.getAccountOf(_alice); assertTrue(hasClaimRecipient_); assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), _alice); @@ -1332,7 +1499,7 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.setClaimRecipient(_bob); - (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); + (, , , hasClaimRecipient_, ) = _wrappedMToken.getAccountOf(_alice); assertTrue(hasClaimRecipient_); assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), _bob); @@ -1340,12 +1507,41 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.setClaimRecipient(address(0)); - (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); + (, , , hasClaimRecipient_, ) = _wrappedMToken.getAccountOf(_alice); assertFalse(hasClaimRecipient_); assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), address(0)); } + /* ============ stopEarningFor batch ============ */ + function test_stopEarningFor_batch_isApprovedEarner() external { + _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); + + 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, false); + _wrappedMToken.setAccountOf(_bob, 0, 0, false, 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_); + } + /* ============ enableEarning ============ */ function test_enableEarning_notApprovedEarner() external { vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, address(_wrappedMToken))); @@ -1412,7 +1608,7 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_100000000000); _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. + _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 500); @@ -1420,7 +1616,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -1428,7 +1624,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false, false); // 1_815 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } @@ -1455,11 +1651,11 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_100000000000); _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. + _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 550); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); @@ -1467,7 +1663,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_210); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false, false); // 1_815 balance with yield. assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_815); } @@ -1494,11 +1690,11 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_100000000000); _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. + _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 50); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -1506,18 +1702,18 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 210); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false, false); // 1_815 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 815); } /* ============ earningPrincipalOf ============ */ function test_earningPrincipalOf() external { - _wrappedMToken.setAccountOf(_alice, 0, 100, false); + _wrappedMToken.setAccountOf(_alice, 0, 100, false, false); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 100); - _wrappedMToken.setAccountOf(_alice, 0, 200, false); + _wrappedMToken.setAccountOf(_alice, 0, 200, false, false); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 200); } @@ -1528,7 +1724,7 @@ contract WrappedMTokenTests is Test { assertFalse(_wrappedMToken.isEarning(_alice)); - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false, false); assertTrue(_wrappedMToken.isEarning(_alice)); } @@ -1552,13 +1748,13 @@ contract WrappedMTokenTests is Test { _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); assertTrue(_wrappedMToken.wasEarningEnabled()); _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); - _wrappedMToken.disableEarning(); + _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); assertTrue(_wrappedMToken.wasEarningEnabled()); } @@ -1569,7 +1765,7 @@ contract WrappedMTokenTests is Test { } function test_claimRecipientFor_hasClaimRecipient() external { - _wrappedMToken.setAccountOf(_alice, 0, 0, true); + _wrappedMToken.setAccountOf(_alice, 0, 0, true, false); _wrappedMToken.setInternalClaimRecipient(_alice, _bob); assertEq(_wrappedMToken.claimRecipientFor(_alice), _bob); @@ -1585,7 +1781,7 @@ contract WrappedMTokenTests is Test { } function test_claimRecipientFor_hasClaimRecipientAndOverrideRecipient() external { - _wrappedMToken.setAccountOf(_alice, 0, 0, true); + _wrappedMToken.setAccountOf(_alice, 0, 0, true, false); _wrappedMToken.setInternalClaimRecipient(_alice, _bob); _registrar.set( @@ -1826,7 +2022,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.currentIndex() ); - _wrappedMToken.setAccountOf(account_, balance_, principal_, false); + _wrappedMToken.setAccountOf(account_, balance_, principal_, false, false); _wrappedMToken.setTotalEarningPrincipal(_wrappedMToken.totalEarningPrincipal() + principal_); _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + balance_); } else { diff --git a/test/utils/EarnerManagerHarness.sol b/test/utils/EarnerManagerHarness.sol new file mode 100644 index 0000000..fb2e798 --- /dev/null +++ b/test/utils/EarnerManagerHarness.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { EarnerManager } from "../../src/EarnerManager.sol"; + +contract EarnerManagerHarness is EarnerManager { + constructor(address registrar_, address migrationAdmin_) EarnerManager(registrar_, migrationAdmin_) {} + + function setInternalEarnerDetails(address account_, address admin_, uint16 feeRate_) external { + _earnerDetails[account_] = EarnerDetails(admin_, feeRate_); + } + + function setDetails(address account_, bool status_, uint16 feeRate_) external { + _setDetails(account_, status_, feeRate_); + } +} diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index bb39b49..41bfd42 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -79,3 +79,23 @@ contract MockRegistrar { listContains[list_][account_] = contains_; } } + +contract MockEarnerManager { + struct EarnerDetails { + bool status; + uint16 feeRate; + address admin; + } + + mapping(address account => EarnerDetails earnerDetails) internal _earnerDetails; + + function setEarnerDetails(address account_, bool status_, uint16 feeRate_, address admin_) external { + _earnerDetails[account_] = EarnerDetails(status_, feeRate_, admin_); + } + + function getEarnerDetails(address account_) external view returns (bool status_, uint16 feeRate_, address admin_) { + EarnerDetails storage earnerDetails_ = _earnerDetails[account_]; + + return (earnerDetails_.status, earnerDetails_.feeRate, earnerDetails_.admin); + } +} diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index 7f9a01f..dd426c9 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -8,9 +8,10 @@ contract WrappedMTokenHarness is WrappedMToken { constructor( address mToken_, address registrar_, + address earnerManager_, address excessDestination_, address migrationAdmin_ - ) WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_) {} + ) WrappedMToken(mToken_, registrar_, earnerManager_, excessDestination_, migrationAdmin_) {} function internalWrap(address account_, address recipient_, uint240 amount_) external returns (uint240 wrapped_) { return _wrap(account_, recipient_, amount_); @@ -36,13 +37,20 @@ contract WrappedMTokenHarness is WrappedMToken { address account_, uint256 balance_, uint256 earningPrincipal_, - bool hasClaimRecipient_ + bool hasClaimRecipient_, + bool hasEarnerDetails_ ) external { - _accounts[account_] = Account(true, uint240(balance_), uint112(earningPrincipal_), hasClaimRecipient_); + _accounts[account_] = Account( + true, + uint240(balance_), + uint112(earningPrincipal_), + hasClaimRecipient_, + hasEarnerDetails_ + ); } function setAccountOf(address account_, uint256 balance_) external { - _accounts[account_] = Account(false, uint240(balance_), 0, false); + _accounts[account_] = Account(false, uint240(balance_), 0, false, false); } function setInternalClaimRecipient(address account_, address claimRecipient_) external { @@ -69,11 +77,31 @@ contract WrappedMTokenHarness is WrappedMToken { roundingError = int144(roundingError_); } + function setHasEarnerDetails(address account_, bool hasEarnerDetails_) external { + _accounts[account_].hasEarnerDetails = hasEarnerDetails_; + } + function getAccountOf( address account_ - ) external view returns (bool isEarning_, uint240 balance_, uint112 earningPrincipal_, bool hasClaimRecipient_) { + ) + external + view + returns ( + bool isEarning_, + uint240 balance_, + uint112 earningPrincipal_, + bool hasClaimRecipient_, + bool hasEarnerDetails_ + ) + { Account storage account = _accounts[account_]; - return (account.isEarning, account.balance, account.earningPrincipal, account.hasClaimRecipient); + return ( + account.isEarning, + account.balance, + account.earningPrincipal, + account.hasClaimRecipient, + account.hasEarnerDetails + ); } function getInternalClaimRecipientOf(address account_) external view returns (address claimRecipient_) { From daa7e03fa2a978ae7f8530cd9acc522b72d32ba8 Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:21:02 -0500 Subject: [PATCH 09/27] feat: test earner accounts involved in migraation (#111) --- src/WrappedMToken.sol | 92 ++++------------------------- src/interfaces/IWrappedMToken.sol | 3 - test/integration/Protocol.t.sol | 43 +++++++------- test/integration/Upgrade.t.sol | 10 +++- test/unit/WrappedMToken.t.sol | 52 ++++------------ test/utils/WrappedMTokenHarness.sol | 4 -- 6 files changed, 55 insertions(+), 149 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index b3e1110..af13b73 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -86,9 +86,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken uint112 public totalEarningPrincipal; - /// @inheritdoc IWrappedMToken - int144 public roundingError; - /// @inheritdoc IWrappedMToken uint240 public totalEarningSupply; @@ -187,9 +184,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (excess_ <= 0) revert NoExcess(); - claimed_ = _getSafeTransferableM(address(this), uint240(uint248(excess_))); - - emit ExcessClaimed(claimed_); + emit ExcessClaimed(claimed_ = uint240(uint248(excess_))); // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. IMTokenLike(mToken).transfer(excessDestination, claimed_); @@ -342,11 +337,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function excess() public view returns (int248 excess_) { unchecked { - int248 earmarked_ = int248(uint248(totalNonEarningSupply + projectedEarningSupply())) + roundingError; - int248 balance_ = int248(uint248(_mBalanceOf(address(this)))); + uint240 earmarked_ = totalNonEarningSupply + projectedEarningSupply(); + uint240 balance_ = _mBalanceOf(address(this)); - // The entire M balance is excess if the total projected supply (factoring rounding errors) is less than 0. - return earmarked_ <= 0 ? balance_ : balance_ - earmarked_; + // The entire M balance is excess if the total projected supply (factoring rounding errors) is 0. + return + earmarked_ == 0 ? int248(uint248(balance_)) : int248(uint248(balance_)) - int248(uint248(earmarked_)); } } @@ -694,7 +690,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @return wrapped_ The amount of wM minted. */ function _wrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 wrapped_) { - _transferFromM(account_, amount_); + // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. + IMTokenLike(mToken).transferFrom(account_, address(this), amount_); + _mint(recipient_, wrapped_ = amount_); } @@ -706,8 +704,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @return unwrapped_ The amount of M withdrawn. */ function _unwrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 unwrapped_) { - _burn(account_, amount_); - _transferM(recipient_, unwrapped_ = amount_); + _burn(account_, unwrapped_ = amount_); + + // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. + IMTokenLike(mToken).transfer(recipient_, amount_); } /** @@ -772,48 +772,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { emit StoppedEarning(account_); } - /** - * @dev Transfer `amount_` M to `recipient_`, tracking this contract's M balance rounding errors. - * @param recipient_ The account to transfer M to. - * @param amount_ The amount of M to transfer. - */ - function _transferM(address recipient_, uint240 amount_) internal { - uint240 startingBalance_ = _mBalanceOf(address(this)); - - // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. - IMTokenLike(mToken).transfer(recipient_, amount_); - - // 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 captured. - uint240 decrease_ = startingBalance_ - _mBalanceOf(address(this)); - - // If the M lost is more than the wM burned, then the difference is added to `roundingError`. - roundingError += int144(int256(uint256(decrease_)) - int256(uint256(amount_))); - } - - /** - * @dev Transfer `amount_` M from `sender_`, tracking this contract's M balance rounding errors. - * @param sender_ The account to transfer M from. - * @param amount_ The amount of M to transfer. - */ - function _transferFromM(address sender_, uint240 amount_) internal { - uint240 startingBalance_ = _mBalanceOf(address(this)); - - // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. - IMTokenLike(mToken).transferFrom(sender_, address(this), _getSafeTransferableM(sender_, 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 more/less than `amount_`. In order to capture the real increase in M, the difference between the - // starting and ending M balance is captured. - uint240 increase_ = _mBalanceOf(address(this)) - startingBalance_; - - // If the M gained is more/less than the wM minted, then the difference is subtracted/added to `roundingError`. - roundingError += int144(int256(uint256(amount_)) - int256(uint256(increase_))); - } - /* ============ Internal View/Pure Functions ============ */ /// @dev Returns the current index of the M Token. @@ -874,30 +832,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return IRegistrarLike(registrar).get(key_); } - /** - * @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 this contract. - * @return safeAmount_ The adjusted amount that can safely be transferred out. - */ - function _getSafeTransferableM(address sender_, uint240 amount_) internal view returns (uint240 safeAmount_) { - // If `sender` is not earning, no need to adjust `amount_`. - if (!IMTokenLike(mToken).isEarning(sender_)) return amount_; - - uint128 currentIndex_ = _currentMIndex(); - uint112 startingPrincipal_ = uint112(IMTokenLike(mToken).principalBalanceOf(sender_)); - uint240 startingBalance_ = IndexingMath.getPresentAmountRoundedDown(startingPrincipal_, currentIndex_); - - // Adjust `amount_` to ensure it's M balance decrement is limited to `amount_`. - unchecked { - uint112 minEndingPrincipal_ = IndexingMath.getPrincipalAmountRoundedUp( - startingBalance_ - amount_, - currentIndex_ - ); - - return IndexingMath.getPresentAmountRoundedDown(startingPrincipal_ - minEndingPrincipal_, currentIndex_); - } - } - /// @dev Returns the address of the contract to use as a migrator, if any. function _getMigrator() internal view override returns (address migrator_) { return diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index b3af5a3..8a34148 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -325,7 +325,4 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the destination where excess is claimed to. function excessDestination() external view returns (address excessDestination); - - /// @notice The amount of rounding error the contract has lost due to rounding in favour of users. - function roundingError() external view returns (int144 roundingError); } diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index 93cb894..debe8ac 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -99,7 +99,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -175,7 +175,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -200,7 +200,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 150_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( int256(_wrapperBalanceOfM), @@ -287,7 +287,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -425,7 +425,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -557,7 +557,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _bobBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.excess(), _excess -= 1); // Assert Bob (Earner) assertEq(_mToken.balanceOf(_bob), _bobBalance); @@ -594,7 +594,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.excess(), _excess -= 1); // Assert Dave (Non-Earner) assertEq(_mToken.balanceOf(_dave), _daveBalance); @@ -608,14 +608,15 @@ contract ProtocolIntegrationTests is TestBase { uint256 vaultStartingBalance_ = _mToken.balanceOf(_excessDestination); - assertEq(_wrappedMToken.claimExcess(), uint256(_excess - 1)); - assertEq(_mToken.balanceOf(_excessDestination), uint256(_excess - 1) + vaultStartingBalance_); + assertEq(_wrappedMToken.claimExcess(), uint256(_excess)); + + assertEq(_mToken.balanceOf(_excessDestination), uint256(_excess) + vaultStartingBalance_); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= _excess); + assertEq(_wrappedMToken.excess(), _excess -= _excess + 1); assertGe( int256(_wrapperBalanceOfM), @@ -624,17 +625,19 @@ contract ProtocolIntegrationTests is TestBase { } function testFuzz_full(uint256 seed_) external { - // TODO: Reinstate to test post-migration for new version. vm.skip(true); + _wrappedMToken.claimExcess(); + for (uint256 index_; index_ < _accounts.length; ++index_) { _giveM(_accounts[index_], 100_000e6); } - for (uint256 index_; index_ < 1000; ++index_) { - 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."); + 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(""); @@ -648,12 +651,10 @@ contract ProtocolIntegrationTests is TestBase { // console2.log(""); // console2.log("--------"); - 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."); - - // 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."); + // 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))); diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 7f5a663..f131b2e 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -111,6 +111,11 @@ contract UpgradeTests is Test, DeployBase { 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(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin()); IWrappedMToken(_WRAPPED_M_TOKEN).migrate(wrappedMTokenMigrator_); @@ -125,6 +130,9 @@ contract UpgradeTests is Test, DeployBase { // Relevant storage slots. assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(), totalEarningSupply_); - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).roundingError(), 0); + + for (uint256 index_; index_ < _earners.length; ++index_) { + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).balanceWithYieldOf(_earners[index_]), balancesWithYield_[index_]); + } } } diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 44c3603..68a1d56 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -906,8 +906,7 @@ contract WrappedMTokenTests is Test { uint128 currentMIndex_, uint240 totalNonEarningSupply_, uint240 totalProjectedEarningSupply_, - uint112 mPrincipalBalance_, - int144 roundingError_ + uint112 mPrincipalBalance_ ) external { currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); @@ -937,13 +936,10 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); - roundingError_ = int144(bound(roundingError_, -1_000_000000, 1_000_000000)); - - _wrappedMToken.setRoundingError(roundingError_); - - uint240 totalProjectedSupply_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; - int248 earmarked_ = int248(uint248(totalProjectedSupply_)) + roundingError_; - int248 excess_ = earmarked_ <= 0 ? int248(uint248(mBalance_)) : int248(uint248(mBalance_)) - earmarked_; + uint240 earmarked_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; + int248 excess_ = earmarked_ <= 0 + ? int248(uint248(mBalance_)) + : int248(uint248(mBalance_)) - int248(uint248(earmarked_)); if (excess_ <= 0) { vm.expectRevert(IWrappedMToken.NoExcess.selector); @@ -1869,41 +1865,21 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.excess(), 0); - _wrappedMToken.setRoundingError(1); - - assertEq(_wrappedMToken.excess(), -1); - _mToken.setBalanceOf(address(_wrappedMToken), 2_101); - assertEq(_wrappedMToken.excess(), 0); + assertEq(_wrappedMToken.excess(), 1); _mToken.setBalanceOf(address(_wrappedMToken), 2_102); - assertEq(_wrappedMToken.excess(), 1); + assertEq(_wrappedMToken.excess(), 2); _mToken.setBalanceOf(address(_wrappedMToken), 3_102); - assertEq(_wrappedMToken.excess(), 1_001); + assertEq(_wrappedMToken.excess(), 1_002); _mToken.setCurrentIndex(1_210000000000); - assertEq(_wrappedMToken.excess(), 891); - - _wrappedMToken.setRoundingError(0); - assertEq(_wrappedMToken.excess(), 892); - - _wrappedMToken.setRoundingError(-1); - - assertEq(_wrappedMToken.excess(), 893); - - _wrappedMToken.setRoundingError(-2_210); - - assertEq(_wrappedMToken.excess(), 3_102); - - _wrappedMToken.setRoundingError(-2_211); - - assertEq(_wrappedMToken.excess(), 3_102); } function testFuzz_excess( @@ -1911,8 +1887,7 @@ contract WrappedMTokenTests is Test { uint128 currentMIndex_, uint240 totalNonEarningSupply_, uint240 totalProjectedEarningSupply_, - uint112 mPrincipalBalance_, - int144 roundingError_ + uint112 mPrincipalBalance_ ) external { currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); @@ -1942,14 +1917,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); - roundingError_ = int144(bound(roundingError_, -1_000_000000, 1_000_000000)); - - _wrappedMToken.setRoundingError(roundingError_); - - uint240 totalProjectedSupply_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; - int248 earmarked_ = int248(uint248(totalProjectedSupply_)) + roundingError_; + uint240 earmarked_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; - assertLe(_wrappedMToken.excess(), int248(uint248(mBalance_)) - earmarked_); + assertLe(_wrappedMToken.excess(), int248(uint248(mBalance_)) - int248(uint248(earmarked_))); } /* ============ totalAccruedYield ============ */ diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index dd426c9..600106b 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -73,10 +73,6 @@ contract WrappedMTokenHarness is WrappedMToken { _enableDisableEarningIndices.push(index_); } - function setRoundingError(int256 roundingError_) external { - roundingError = int144(roundingError_); - } - function setHasEarnerDetails(address account_, bool hasEarnerDetails_) external { _accounts[account_].hasEarnerDetails = hasEarnerDetails_; } From 5aa9684e0476eb7ec2c459afcee2d290d119372d Mon Sep 17 00:00:00 2001 From: toninorair Date: Sun, 2 Mar 2025 22:51:07 -0500 Subject: [PATCH 10/27] Rebase on main v2 branch (#115) Co-authored-by: Michael De Luca --- src/WrappedMToken.sol | 66 +---- src/WrappedMTokenMigratorV1.sol | 64 +++++ src/interfaces/IWrappedMToken.sol | 16 +- test/integration/Migration.sol | 241 ++++++++++++++++++ test/unit/WrappedMToken.t.sol | 375 +++++++++++++++++----------- test/utils/WrappedMTokenHarness.sol | 8 +- 6 files changed, 568 insertions(+), 202 deletions(-) create mode 100644 test/integration/Migration.sol diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index af13b73..7d45ba3 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -95,8 +95,11 @@ 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; @@ -195,17 +198,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (!_isThisApprovedEarner()) revert NotApprovedEarner(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 @@ -213,29 +208,23 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (_isThisApprovedEarner()) revert IsApprovedEarner(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 { - if (!isEarningEnabled()) revert EarningIsDisabled(); - - // NOTE: Use `currentIndex()` if/when upgrading to support `startEarningFor` while earning is disabled. - _startEarningFor(account_, _currentMIndex()); + _startEarningFor(account_, currentIndex()); } /// @inheritdoc IWrappedMToken function startEarningFor(address[] calldata accounts_) external { if (!isEarningEnabled()) revert EarningIsDisabled(); - // NOTE: Use `currentIndex()` if/when upgrading to support `startEarningFor` while earning is disabled. - uint128 currentIndex_ = _currentMIndex(); + uint128 currentIndex_ = currentIndex(); for (uint256 index_; index_ < accounts_.length; ++index_) { _startEarningFor(accounts_[index_], currentIndex_); @@ -314,9 +303,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function currentIndex() public view returns (uint128 index_) { - if (isEarningEnabled()) return _currentMIndex(); + uint128 disableIndex_ = disableIndex == 0 ? IndexingMath.EXP_SCALED_ONE : disableIndex; - return UIntMath.max128(IndexingMath.EXP_SCALED_ONE, _lastDisableEarningIndex()); + return enableMIndex == 0 ? disableIndex_ : (disableIndex_ * _currentMIndex()) / enableMIndex; } /// @inheritdoc IWrappedMToken @@ -326,12 +315,7 @@ 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 @@ -786,11 +770,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, address(this)); } - /// @dev Returns the earning index from the last `disableEarning` call. - function _lastDisableEarningIndex() internal view returns (uint128 index_) { - return wasEarningEnabled() ? _unsafeAccess(_enableDisableEarningIndices, 1) : 0; - } - /** * @dev Compute the yield given an account's balance, earning principal, and the current index. * @param balance_ The token balance of an earning account. @@ -868,23 +847,4 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function _revertIfInvalidRecipient(address account_) internal pure { if (account_ == address(0)) revert InvalidRecipient(account_); } - - /** - * @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_) - } - } - } } diff --git a/src/WrappedMTokenMigratorV1.sol b/src/WrappedMTokenMigratorV1.sol index 4dd3685..9d6947b 100644 --- a/src/WrappedMTokenMigratorV1.sol +++ b/src/WrappedMTokenMigratorV1.sol @@ -11,6 +11,9 @@ import { ListOfEarnersToMigrate } from "./ListOfEarnersToMigrate.sol"; * @author M^0 Labs */ contract WrappedMTokenMigratorV1 { + /// @notice Emitted when the `enableDisableEarningIndices` array has an invalid length. + error InvalidEnableDisableEarningIndicesArrayLength(); + /* ============ Structs ============ */ /** @@ -57,6 +60,14 @@ contract WrappedMTokenMigratorV1 { fallback() external virtual { _migrateEarners(); + (bool earningEnabled_, uint128 disableIndex_) = _clearEnableDisableEarningIndices(); + + if (earningEnabled_) { + _setEnableMIndex(IndexingMath.EXP_SCALED_ONE); + } else { + _setDisableIndex(disableIndex_); + } + address newImplementation_ = newImplementation; assembly { @@ -98,4 +109,57 @@ contract WrappedMTokenMigratorV1 { 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/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 8a34148..e62c180 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -29,13 +29,13 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /** * @notice Emitted when Wrapped M earning is enabled. - * @param index The index at the moment earning is enabled. + * @param index The M index at the moment earning is enabled. */ event EarningEnabled(uint128 index); /** * @notice Emitted when Wrapped M earning is disabled. - * @param index The index at the moment earning is disabled. + * @param index The WrappedM index at the moment earning is disabled. */ event EarningDisabled(uint128 index); @@ -65,9 +65,6 @@ 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 an earner. * @param account The account that is an approved earner. @@ -280,9 +277,15 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The current index of Wrapped M's earning mechanism. function currentIndex() external view returns (uint128 index); + /// @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 (int248 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. @@ -293,9 +296,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Whether Wrapped M earning is enabled. function isEarningEnabled() external view returns (bool isEnabled); - /// @notice Whether Wrapped M earning has been enabled at least once. - function wasEarningEnabled() external view returns (bool wasEnabled); - /// @notice The account that can bypass the Registrar and call the `migrate(address migrator)` function. function migrationAdmin() external view returns (address migrationAdmin); diff --git a/test/integration/Migration.sol b/test/integration/Migration.sol new file mode 100644 index 0000000..a14b025 --- /dev/null +++ b/test/integration/Migration.sol @@ -0,0 +1,241 @@ +// 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 test_initialState() external view { + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + } + + function test_index_noMigration() external { + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 18_745_425_119664); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 184_519_881653); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 290_781_743197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_542352); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 955_599930); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 69_560_490365); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 364_097752); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_771_521603); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 62_962_533531); + + assertEq(_wrappedMToken.currentIndex(), 1_073787769981); + } + + function test_index_migrate_earningNotDisabled() external { + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 0); + assertEq(_wrappedMToken.enableMIndex(), IndexingMath.EXP_SCALED_ONE); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 18_745_425_119664); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 184_519_881653); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 290_781_743197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_542352); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 955_599930); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 69_560_490365); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 364_097752); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_771_521603); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 62_962_533531); + + assertEq(_wrappedMToken.currentIndex(), 1_073787769981); + } + + function test_index_migrate_earningDisabled_immediatelyReenabled() external { + _removeFromList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.disableEarning(); + + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.enableMIndex(), 0); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + _addToList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.enableEarning(); + + assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.enableMIndex(), 1_023463403719); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 18_745_425_119664 - 35); // Rounding error + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 184_519_881653); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 290_781_743197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_542352); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 955_599930); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 69_560_490365); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 364_097752); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_771_521603); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 62_962_533531); + + assertEq(_wrappedMToken.currentIndex(), 1_073787769981 - 2); // Rounding error + } + + function test_index_migrate_earningDisabled_notReenabled() external { + _removeFromList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.disableEarning(); + + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.enableMIndex(), 0); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + } + + function test_index_migrate_earningDisabled_reenabledLater() external { + _removeFromList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.disableEarning(); + + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.enableMIndex(), 0); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + _addToList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.enableEarning(); + + assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.enableMIndex(), 1_073787769979); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 18_745_425_119664 - 35); // Rounding error + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 184_519_881653); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 290_781_743197); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_542352); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 955_599930); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 69_560_490365); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 364_097752); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_771_521603); + assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 62_962_533531); + + assertEq(_wrappedMToken.currentIndex(), 1_073787769981 - 2); // Rounding error + } +} diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 68a1d56..a94169c 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -80,6 +80,8 @@ contract WrappedMTokenTests is Test { 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 { @@ -169,8 +171,8 @@ contract WrappedMTokenTests is Test { } function test_wrap_toEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _mToken.setBalanceOf(_alice, 1_002); @@ -242,11 +244,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -296,11 +304,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -348,11 +362,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -400,11 +420,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -453,8 +479,8 @@ contract WrappedMTokenTests is Test { } function test_internalUnwrap_insufficientBalance_fromEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 999, 909, false, false); @@ -464,8 +490,8 @@ contract WrappedMTokenTests is Test { function test_internalUnwrap_fromNonEarner() external { _mToken.setIsEarning(address(_wrappedMToken), true); - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _mToken.setBalanceOf(address(_wrappedMToken), 1_000); @@ -523,8 +549,8 @@ contract WrappedMTokenTests is Test { function test_internalUnwrap_fromEarner() external { _mToken.setIsEarning(address(_wrappedMToken), true); - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _mToken.setBalanceOf(address(_wrappedMToken), 1_000); @@ -596,11 +622,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 unwrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -644,11 +676,17 @@ contract WrappedMTokenTests is Test { bool accountEarning_, uint240 balanceWithYield_, uint240 balance_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -688,8 +726,8 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); @@ -716,8 +754,8 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner_withOverrideRecipient() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _registrar.set( keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), @@ -756,8 +794,8 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner_withFee() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, true); _earnerManager.setEarnerDetails(_alice, true, 1_500, _bob); @@ -780,8 +818,8 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner_withFeeAboveOneHundredPercent() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, true); _earnerManager.setEarnerDetails(_alice, true, type(uint16).max, _bob); @@ -804,8 +842,8 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner_withOverrideRecipientAndFee() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _registrar.set( keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), @@ -842,12 +880,18 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, bool claimOverride_, uint16 feeRate_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -904,13 +948,19 @@ contract WrappedMTokenTests is Test { function testFuzz_claimExcess( bool earningEnabled_, uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, uint240 totalNonEarningSupply_, uint240 totalProjectedEarningSupply_, uint112 mPrincipalBalance_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); uint240 maxAmount_ = _getMaxAmount(_wrappedMToken.currentIndex()); @@ -982,8 +1032,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_insufficientBalance_fromEarner_toNonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. @@ -1043,8 +1093,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromEarner_toNonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); @@ -1092,8 +1142,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromNonEarner_toEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(500); _wrappedMToken.setTotalEarningSupply(500); @@ -1124,8 +1174,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromEarner_toEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_500); _wrappedMToken.setTotalEarningSupply(1_500); @@ -1173,8 +1223,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_earnerToSelf() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); @@ -1208,11 +1258,17 @@ contract WrappedMTokenTests is Test { uint240 bobBalanceWithYield_, uint240 bobBalance_, uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, uint240 amount_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (aliceBalanceWithYield_, aliceBalance_) = _getFuzzedBalances( aliceBalanceWithYield_, @@ -1263,23 +1319,16 @@ contract WrappedMTokenTests is Test { } /* ============ startEarningFor ============ */ - function test_startEarningFor_earningIsDisabled() external { - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.startEarningFor(_alice); - } - function test_startEarningFor_notApprovedEarner() external { - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, _alice)); _wrappedMToken.startEarningFor(_alice); } function test_startEarning_overflow() external { - _mToken.setCurrentIndex(1_000000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - uint240 aliceBalance_ = uint240(type(uint112).max) + 1; + uint240 aliceBalance_ = uint240(type(uint112).max) + 20; // TODO: _getMaxAmount(1_100000000000) + 2; ? _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); @@ -1292,8 +1341,8 @@ contract WrappedMTokenTests is Test { } function test_startEarningFor() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalNonEarningSupply(1_000); @@ -1315,30 +1364,34 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 1_000); } - function testFuzz_startEarningFor(bool earningEnabled_, uint240 balance_, uint128 currentMIndex_) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + function testFuzz_startEarningFor( + bool earningEnabled_, + uint240 balance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); uint128 currentIndex_ = _wrappedMToken.currentIndex(); balance_ = uint240(bound(balance_, 0, _getMaxAmount(currentIndex_))); - _setupAccount(_alice, false, balance_, balance_); + _setupAccount(_alice, false, 0, balance_); _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); - if (earningEnabled_) { - vm.expectEmit(); - emit IWrappedMToken.StartedEarning(_alice); - } else { - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - } + vm.expectEmit(); + emit IWrappedMToken.StartedEarning(_alice); _wrappedMToken.startEarningFor(_alice); - if (!earningEnabled_) return; - uint112 earningPrincipal_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); assertEq(_wrappedMToken.isEarning(_alice), true); @@ -1357,9 +1410,8 @@ contract WrappedMTokenTests is Test { } function test_startEarningFor_batch_notApprovedEarner() external { - _mToken.setCurrentIndex(1_100000000000); - - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); @@ -1372,9 +1424,8 @@ contract WrappedMTokenTests is Test { } function test_startEarningFor_batch() external { - _mToken.setCurrentIndex(1_100000000000); - - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); @@ -1401,8 +1452,8 @@ contract WrappedMTokenTests is Test { } function test_stopEarningFor() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); @@ -1437,11 +1488,17 @@ contract WrappedMTokenTests is Test { bool earningEnabled_, uint240 balanceWithYield_, uint240 balance_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -1547,14 +1604,18 @@ contract WrappedMTokenTests is Test { function test_enableEarning() external { _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _mToken.setCurrentIndex(1_100000000000); + _mToken.setCurrentIndex(1_210000000000); + + assertEq(_wrappedMToken.enableMIndex(), 0); + assertEq(_wrappedMToken.currentIndex(), 1_000000000000); vm.expectEmit(); - emit IWrappedMToken.EarningEnabled(1_100000000000); + emit IWrappedMToken.EarningEnabled(1_210000000000); _wrappedMToken.enableEarning(); - assertEq(_wrappedMToken.currentIndex(), 1_100000000000); + assertEq(_wrappedMToken.enableMIndex(), 1_210000000000); + assertEq(_wrappedMToken.currentIndex(), 1_000000000000); } /* ============ disableEarning ============ */ @@ -1571,21 +1632,27 @@ contract WrappedMTokenTests is Test { } function test_disableEarning() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _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_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1601,8 +1668,8 @@ contract WrappedMTokenTests is Test { } function test_balanceOf_earner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. @@ -1627,8 +1694,8 @@ contract WrappedMTokenTests is Test { /* ============ balanceWithYieldOf ============ */ function test_balanceWithYieldOf_nonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1644,8 +1711,8 @@ contract WrappedMTokenTests is Test { } function test_balanceWithYieldOf_earner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. @@ -1655,7 +1722,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); - _mToken.setCurrentIndex(1_210000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_210); @@ -1666,8 +1733,8 @@ contract WrappedMTokenTests is Test { /* ============ accruedYieldOf ============ */ function test_accruedYieldOf_nonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1683,8 +1750,8 @@ contract WrappedMTokenTests is Test { } function test_accruedYieldOf_earner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. @@ -1694,7 +1761,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); - _mToken.setCurrentIndex(1_210000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 210); @@ -1729,30 +1796,17 @@ contract WrappedMTokenTests is Test { function test_isEarningEnabled() external { assertFalse(_wrappedMToken.isEarningEnabled()); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); assertTrue(_wrappedMToken.isEarningEnabled()); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _wrappedMToken.setEnableMIndex(0); assertFalse(_wrappedMToken.isEarningEnabled()); - } - - /* ============ wasEarningEnabled ============ */ - function test_wasEarningEnabled() external { - assertFalse(_wrappedMToken.wasEarningEnabled()); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - assertTrue(_wrappedMToken.wasEarningEnabled()); + _wrappedMToken.setEnableMIndex(1_100000000000); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); - - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - - assertTrue(_wrappedMToken.wasEarningEnabled()); + assertTrue(_wrappedMToken.isEarningEnabled()); } /* ============ claimRecipientFor ============ */ @@ -1829,31 +1883,47 @@ contract WrappedMTokenTests is Test { function test_currentIndex() external { assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); - _mToken.setCurrentIndex(1_100000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _wrappedMToken.setDisableIndex(1_050000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_050000000000); + + _wrappedMToken.setDisableIndex(1_100000000000); assertEq(_wrappedMToken.currentIndex(), 1_100000000000); - _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - assertEq(_wrappedMToken.currentIndex(), 1_210000000000); + assertEq(_wrappedMToken.currentIndex(), 1_331000000000); + + _wrappedMToken.setEnableMIndex(1_155000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_210000000000); + assertEq(_wrappedMToken.currentIndex(), 1_267619047619); + + _wrappedMToken.setEnableMIndex(1_210000000000); assertEq(_wrappedMToken.currentIndex(), 1_210000000000); - _mToken.setCurrentIndex(1_331000000000); + _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_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); assertEq(_wrappedMToken.excess(), 0); @@ -1877,7 +1947,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.excess(), 1_002); - _mToken.setCurrentIndex(1_210000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.excess(), 892); } @@ -1885,13 +1955,19 @@ contract WrappedMTokenTests is Test { function testFuzz_excess( bool earningEnabled_, uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, uint240 totalNonEarningSupply_, uint240 totalProjectedEarningSupply_, uint112 mPrincipalBalance_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); uint240 maxAmount_ = _getMaxAmount(_wrappedMToken.currentIndex()); @@ -1924,8 +2000,8 @@ contract WrappedMTokenTests is Test { /* ============ totalAccruedYield ============ */ function test_totalAccruedYield() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(909); _wrappedMToken.setTotalEarningSupply(1_000); @@ -1940,7 +2016,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 200); - _mToken.setCurrentIndex(1_210000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.totalAccruedYield(), 310); } @@ -1958,12 +2034,33 @@ contract WrappedMTokenTests is Test { return (uint240(type(uint112).max) * index_) / _EXP_SCALED_ONE; } - function _setupIndexes(bool earningEnabled_, uint128 currentMIndex_) internal { + 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.pushEnableDisableEarningIndex(_EXP_SCALED_ONE); + _wrappedMToken.setEnableMIndex(enableMIndex_); } } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index 600106b..2c7459f 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -69,8 +69,12 @@ contract WrappedMTokenHarness is WrappedMToken { totalEarningPrincipal = uint112(totalEarningPrincipal_); } - function pushEnableDisableEarningIndex(uint128 index_) external { - _enableDisableEarningIndices.push(index_); + function setEnableMIndex(uint256 enableMIndex_) external { + enableMIndex = uint128(enableMIndex_); + } + + function setDisableIndex(uint256 disableIndex_) external { + disableIndex = uint128(disableIndex_); } function setHasEarnerDetails(address account_, bool hasEarnerDetails_) external { From 1ed3ab372cf9d2b38ec237c39015b0616e806206 Mon Sep 17 00:00:00 2001 From: toninorair Date: Fri, 14 Mar 2025 00:14:45 -0400 Subject: [PATCH 11/27] Fixes after Blackthorn and internal audits (#117) --- src/WrappedMToken.sol | 134 +++++++----- src/interfaces/IWrappedMToken.sol | 38 +--- test/integration/Protocol.t.sol | 155 +++++++++++--- test/integration/TestBase.sol | 13 -- .../vendor/protocol/Interfaces.sol | 2 + test/unit/Stories.t.sol | 43 ++-- test/unit/WrappedMToken.t.sol | 195 +++++------------- test/utils/WrappedMTokenHarness.sol | 12 +- 8 files changed, 299 insertions(+), 293 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 7d45ba3..c74cc36 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -101,6 +101,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken uint128 public disableIndex; + /// @inheritdoc IWrappedMToken + int240 public roundingError; + mapping(address account => address claimRecipient) internal _claimRecipients; /* ============ Constructor ============ */ @@ -131,13 +134,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /* ============ Interactive Functions ============ */ /// @inheritdoc IWrappedMToken - function wrap(address recipient_, uint256 amount_) external returns (uint240 wrapped_) { - return _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); - } - - /// @inheritdoc IWrappedMToken - function wrap(address recipient_) external returns (uint240 wrapped_) { - return _wrap(msg.sender, recipient_, _mBalanceOf(msg.sender)); + function wrap(address recipient_, uint256 amount_) external { + _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); } /// @inheritdoc IWrappedMToken @@ -148,32 +146,22 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { uint8 v_, bytes32 r_, bytes32 s_ - ) external returns (uint240 wrapped_) { - IMTokenLike(mToken).permit(msg.sender, address(this), amount_, deadline_, v_, r_, s_); + ) external { + try IMTokenLike(mToken).permit(msg.sender, address(this), amount_, deadline_, v_, r_, s_) {} catch {} - return _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); + _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); } /// @inheritdoc IWrappedMToken - function wrapWithPermit( - address recipient_, - uint256 amount_, - uint256 deadline_, - bytes memory signature_ - ) external returns (uint240 wrapped_) { - IMTokenLike(mToken).permit(msg.sender, address(this), amount_, deadline_, signature_); - - return _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); - } + function wrapWithPermit(address recipient_, uint256 amount_, uint256 deadline_, bytes memory signature_) external { + try IMTokenLike(mToken).permit(msg.sender, address(this), amount_, deadline_, signature_) {} catch {} - /// @inheritdoc IWrappedMToken - function unwrap(address recipient_, uint256 amount_) external returns (uint240 unwrapped_) { - return _unwrap(msg.sender, recipient_, UIntMath.safe240(amount_)); + _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); } /// @inheritdoc IWrappedMToken - function unwrap(address recipient_) external returns (uint240 unwrapped_) { - return _unwrap(msg.sender, recipient_, uint240(balanceOf(msg.sender))); + function unwrap(address recipient_, uint256 amount_) external { + _unwrap(msg.sender, recipient_, UIntMath.safe240(amount_)); } /// @inheritdoc IWrappedMToken @@ -183,11 +171,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function claimExcess() external returns (uint240 claimed_) { - int248 excess_ = excess(); + int240 excess_ = excess(); if (excess_ <= 0) revert NoExcess(); - emit ExcessClaimed(claimed_ = uint240(uint248(excess_))); + emit ExcessClaimed(claimed_ = uint240(excess_)); // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. IMTokenLike(mToken).transfer(excessDestination, claimed_); @@ -217,6 +205,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function startEarningFor(address account_) external { + if (!isEarningEnabled()) revert EarningIsDisabled(); + _startEarningFor(account_, currentIndex()); } @@ -305,7 +295,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function currentIndex() public view returns (uint128 index_) { uint128 disableIndex_ = disableIndex == 0 ? IndexingMath.EXP_SCALED_ONE : disableIndex; - return enableMIndex == 0 ? disableIndex_ : (disableIndex_ * _currentMIndex()) / enableMIndex; + unchecked { + return + enableMIndex == 0 + ? disableIndex_ + : UIntMath.safe128((uint256(disableIndex_) * _currentMIndex()) / enableMIndex); + } } /// @inheritdoc IWrappedMToken @@ -319,24 +314,20 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /// @inheritdoc IWrappedMToken - function excess() public view returns (int248 excess_) { + function excess() public view returns (int240 excess_) { unchecked { uint240 earmarked_ = totalNonEarningSupply + projectedEarningSupply(); uint240 balance_ = _mBalanceOf(address(this)); - // The entire M balance is excess if the total projected supply (factoring rounding errors) is 0. - return - earmarked_ == 0 ? int248(uint248(balance_)) : int248(uint248(balance_)) - int248(uint248(earmarked_)); + // Decreases claimable excess by the `roundingError` for extra level of safety and solvency. + return int240(balance_) - int240(earmarked_) - roundingError; } } /// @inheritdoc IWrappedMToken function totalAccruedYield() external view returns (uint240 yield_) { - uint240 projectedEarningSupply_ = projectedEarningSupply(); - uint240 earningSupply_ = totalEarningSupply; - unchecked { - return projectedEarningSupply_ <= earningSupply_ ? 0 : projectedEarningSupply_ - earningSupply_; + return projectedEarningSupply() - totalEarningSupply; } } @@ -427,15 +418,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { */ function _addEarningAmount(address account_, uint240 amount_, uint128 currentIndex_) internal { Account storage accountInfo_ = _accounts[account_]; - uint112 principal_ = IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_); + + // 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 { accountInfo_.balance += amount_; - accountInfo_.earningPrincipal = UIntMath.safe112(uint256(accountInfo_.earningPrincipal) + principal_); + accountInfo_.earningPrincipal = UIntMath.safe112(uint256(accountInfo_.earningPrincipal) + principalDown_); } - _addTotalEarningSupply(amount_, principal_); + _addTotalEarningSupply(amount_, principalUp_); } /** @@ -452,18 +447,18 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { uint112 earningPrincipal_ = accountInfo_.earningPrincipal; - // `min112` prevents `earningPrincipal` underflow. - uint112 principal_ = UIntMath.min112( - IndexingMath.getPrincipalAmountRoundedUp(amount_, currentIndex_), - 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_; - accountInfo_.earningPrincipal = earningPrincipal_ - principal_; + // `min112` prevents `earningPrincipal` underflow. + accountInfo_.earningPrincipal = earningPrincipal_ - UIntMath.min112(principalUp_, earningPrincipal_); } - _subtractTotalEarningSupply(amount_, principal_); + _subtractTotalEarningSupply(amount_, principalDown_); } /** @@ -671,13 +666,27 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param account_ The account from which M is deposited. * @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_) { + function _wrap(address account_, address recipient_, uint240 amount_) internal { + uint240 startingBalance_ = _mBalanceOf(address(this)); + // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. IMTokenLike(mToken).transferFrom(account_, address(this), amount_); - _mint(recipient_, wrapped_ = amount_); + // NOTE: Computes the actual increase in the $M balance of the `WrappedM` contract and tracks potential $M rounding adjustments. + // 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. + // Result: `roundingError` remains the same or decreases. + // + // 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 deducted from $M locked in `WrappedM` compared to the minted amount of Wrapped $M. + // Result: `roundingError` remains the same or increases. + uint240 endingBalance_ = _mBalanceOf(address(this)); + uint240 mIncrease_ = endingBalance_ - startingBalance_; + roundingError += int240(amount_) - int240(mIncrease_); + + // Mints precise amount of Wrapped $M to `recipient_`. + _mint(recipient_, amount_); } /** @@ -685,13 +694,23 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @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. */ - function _unwrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 unwrapped_) { - _burn(account_, unwrapped_ = amount_); + function _unwrap(address account_, address recipient_, uint240 amount_) internal { + _burn(account_, amount_); + + uint240 startingBalance_ = _mBalanceOf(address(this)); // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. IMTokenLike(mToken).transfer(recipient_, amount_); + + // 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 → rounds up. + // Option 2: $M transfer from an $M earner (`WrappedM` in earning state) to an $M non-earner → 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. + // Result: `roundingError` remains the same or increases. + uint240 endingBalance_ = _mBalanceOf(address(this)); + uint240 mDecrease_ = startingBalance_ - endingBalance_; + roundingError += int240(mDecrease_) - int240(amount_); } /** @@ -709,14 +728,17 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (accountInfo_.isEarning) return; uint240 balance_ = accountInfo_.balance; - uint112 earningPrincipal_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); + + // 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 = earningPrincipal_; + accountInfo_.earningPrincipal = principalDown_; accountInfo_.hasEarnerDetails = admin_ != address(0); // Has earner details if an admin exists for this account. - _addTotalEarningSupply(balance_, earningPrincipal_); - + _addTotalEarningSupply(balance_, principalUp_); unchecked { totalNonEarningSupply -= balance_; } diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index e62c180..6d9a11e 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -111,17 +111,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /** * @notice Wraps `amount` M from the caller into wM for `recipient`. * @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); - - /** - * @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); + function wrap(address recipient, uint256 amount) external; /** * @notice Wraps `amount` M from the caller into wM for `recipient`, using a permit. @@ -131,7 +123,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { * @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). - * @return wrapped The amount of wM minted. */ function wrapWithPermit( address recipient, @@ -140,7 +131,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { uint8 v, bytes32 r, bytes32 s - ) external returns (uint240 wrapped); + ) external; /** * @notice Wraps `amount` M from the caller into wM for `recipient`, using a permit. @@ -148,29 +139,15 @@ interface IWrappedMToken is IMigratable, IERC20Extended { * @param amount The amount of M deposited. * @param deadline The last timestamp where the signature is still valid. * @param signature An arbitrary signature (EIP-712). - * @return wrapped The amount of wM minted. */ - function wrapWithPermit( - address recipient, - uint256 amount, - uint256 deadline, - bytes memory signature - ) external returns (uint240 wrapped); + function wrapWithPermit(address recipient, uint256 amount, uint256 deadline, bytes memory signature) external; /** * @notice Unwraps `amount` wM from the caller into M for `recipient`. * @param recipient The account receiving the withdrawn M. * @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`. @@ -281,7 +258,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { 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 (int248 excess); + function excess() external view returns (int240 excess); /// @notice The wrapper's index when earning was most recently disabled. function disableIndex() external view returns (uint128 disableIndex); @@ -325,4 +302,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the destination where excess is claimed to. function excessDestination() external view returns (address excessDestination); + + /// @notice The rounding error that may occur due to imprecise $M transfers in and out of WrappedM contract. + function roundingError() external view returns (int240 roundingError); } diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index debe8ac..bc67d27 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -2,7 +2,7 @@ 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"; @@ -98,8 +98,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -124,6 +125,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 50_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -174,8 +176,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.roundingError(), 2); assertGe( int256(_wrapperBalanceOfM), @@ -200,7 +203,8 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 150_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.roundingError(), 3); assertGe( int256(_wrapperBalanceOfM), @@ -222,6 +226,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1_190592); assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.roundingError(), 3); assertGe( int256(_wrapperBalanceOfM), @@ -286,8 +291,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -336,8 +342,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _bobBalance - _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + _aliceBalance); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.roundingError(), 1); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += _aliceBalance); @@ -365,8 +372,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 50_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 50_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -424,8 +432,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -456,8 +465,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -490,6 +500,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614473 + 1); assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -508,8 +519,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _carolBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _carolBalance); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -539,6 +551,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _aliceBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); + assertEq(_wrappedMToken.roundingError(), 1); // Assert Alice (Non-Earner) assertEq(_mToken.balanceOf(_alice), _aliceBalance); @@ -556,8 +569,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _bobBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 3); + assertEq(_wrappedMToken.roundingError(), 2); // Assert Bob (Earner) assertEq(_mToken.balanceOf(_bob), _bobBalance); @@ -575,8 +589,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _carolBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess += 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.roundingError(), 2); // Assert Carol (Earner) assertEq(_mToken.balanceOf(_carol), _carolBalance); @@ -594,7 +609,8 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.roundingError(), 3); // Assert Dave (Non-Earner) assertEq(_mToken.balanceOf(_dave), _daveBalance); @@ -616,7 +632,8 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= _excess + 1); + assertEq(_wrappedMToken.excess(), _excess -= _excess); + assertEq(_wrappedMToken.roundingError(), 3); assertGe( int256(_wrapperBalanceOfM), @@ -692,7 +709,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_); @@ -719,7 +736,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_); @@ -759,6 +776,92 @@ contract ProtocolIntegrationTests is TestBase { } } + function test_integration_roundingError() external { + // Alice is $M earner, bob is not + vm.prank(_alice); + _mToken.startEarning(); + + _giveM(_alice, 100_000000); + _giveM(_bob, 100_000000); + + // Wraps for earners and non-earners with different values of $M index (timestamps) + uint256 mBalanceBeforeWrap_ = _mToken.balanceOf(address(_wrappedMToken)); + _wrap(_alice, _alice, 10_000000); + uint256 mBalanceAfterWrap_ = _mToken.balanceOf(address(_wrappedMToken)); + + uint256 mDelta = mBalanceAfterWrap_ - mBalanceBeforeWrap_; + + assertEq(mDelta, 10000001); + assertEq(_wrappedMToken.roundingError(), -1); + + mBalanceBeforeWrap_ = _mToken.balanceOf(address(_wrappedMToken)); + _wrap(_bob, _bob, 10_000000); + mBalanceAfterWrap_ = _mToken.balanceOf(address(_wrappedMToken)); + + mDelta = mBalanceAfterWrap_ - mBalanceBeforeWrap_; + + assertEq(mDelta, 10000000); + assertEq(_wrappedMToken.roundingError(), -1); + + vm.warp(vm.getBlockTimestamp() + 5 seconds); + + mBalanceBeforeWrap_ = _mToken.balanceOf(address(_wrappedMToken)); + _wrap(_bob, _bob, 10_000000); + mBalanceAfterWrap_ = _mToken.balanceOf(address(_wrappedMToken)); + + mDelta = mBalanceAfterWrap_ - mBalanceBeforeWrap_; + + assertEq(mDelta, 9999999); + assertEq(_wrappedMToken.roundingError(), 0); + + mBalanceBeforeWrap_ = _mToken.balanceOf(address(_wrappedMToken)); + _wrap(_alice, _alice, 10_000000); + mBalanceAfterWrap_ = _mToken.balanceOf(address(_wrappedMToken)); + + mDelta = mBalanceAfterWrap_ - mBalanceBeforeWrap_; + + assertEq(mDelta, 10000000); + assertEq(_wrappedMToken.roundingError(), 0); + + // Unwraps + + uint256 mBalanceBeforeUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); + _unwrap(_alice, _alice, 10_000000); + uint256 mBalanceAfterUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); + mDelta = mBalanceBeforeUnwrap_ - mBalanceAfterUnwrap_; + + assertEq(mDelta, 10000000); + assertEq(_wrappedMToken.roundingError(), 0); + + mBalanceBeforeUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); + _unwrap(_bob, _bob, 10_000000); + mBalanceAfterUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); + mDelta = mBalanceBeforeUnwrap_ - mBalanceAfterUnwrap_; + + assertEq(mDelta, 10_000000); + assertEq(_wrappedMToken.roundingError(), 0); + + vm.warp(vm.getBlockTimestamp() + 10 minutes); + + mBalanceBeforeUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); + _unwrap(_alice, _alice, 10_000000); + mBalanceAfterUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); + mDelta = mBalanceBeforeUnwrap_ - mBalanceAfterUnwrap_; + + assertEq(mDelta, 10_000001); + assertEq(_wrappedMToken.roundingError(), 1); + + vm.warp(vm.getBlockTimestamp() + 90 minutes); + + mBalanceBeforeUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); + _unwrap(_bob, _bob, 10_000000); + mBalanceAfterUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); + mDelta = mBalanceBeforeUnwrap_ - mBalanceAfterUnwrap_; + + assertEq(mDelta, 10_000001); + assertEq(_wrappedMToken.roundingError(), 2); + } + function _getNewSeed(uint256 seed_) internal pure returns (uint256 newSeed_) { return uint256(keccak256(abi.encodePacked(seed_))); } diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 3697d4c..33ca17a 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -151,14 +151,6 @@ contract TestBase is Test { _wrappedMToken.wrap(recipient_, amount_); } - function _wrap(address account_, address recipient_) internal { - vm.prank(account_); - _mToken.approve(address(_wrappedMToken), type(uint256).max); - - vm.prank(account_); - _wrappedMToken.wrap(recipient_); - } - function _wrapWithPermitVRS( address account_, uint256 signerPrivateKey_, @@ -192,11 +184,6 @@ contract TestBase is Test { _wrappedMToken.unwrap(recipient_, amount_); } - function _unwrap(address account_, address recipient_) internal { - vm.prank(account_); - _wrappedMToken.unwrap(recipient_); - } - function _transferWM(address sender_, address recipient_, uint256 amount_) internal { vm.prank(sender_); _wrappedMToken.transfer(recipient_, amount_); diff --git a/test/integration/vendor/protocol/Interfaces.sol b/test/integration/vendor/protocol/Interfaces.sol index ccdcb04..dc549ec 100644 --- a/test/integration/vendor/protocol/Interfaces.sol +++ b/test/integration/vendor/protocol/Interfaces.sol @@ -24,4 +24,6 @@ interface IMTokenLike { function totalEarningSupply() external view returns (uint240 supply); function totalNonEarningSupply() external view returns (uint240 supply); + + function startEarning() external; } diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index 75d4a63..acb6fa9 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -203,8 +203,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 300_000000); assertEq(_wrappedMToken.totalSupply(), 500_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 149_999998); - assertEq(_wrappedMToken.excess(), 250_000002); + assertEq(_wrappedMToken.totalAccruedYield(), 150_000001); + assertEq(_wrappedMToken.excess(), 249_999999); vm.prank(_dave); _wrappedMToken.transfer(_bob, 50_000000); @@ -221,8 +221,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalSupply(), 500_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 149_999996); - assertEq(_wrappedMToken.excess(), 250_000004); + 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 @@ -247,8 +247,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 250_000000); assertEq(_wrappedMToken.totalSupply(), 500_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 283_333328); - assertEq(_wrappedMToken.excess(), 416_666672); + assertEq(_wrappedMToken.totalAccruedYield(), 283_333336); + assertEq(_wrappedMToken.excess(), 416_666664); _earnerManager.setEarnerDetails(_alice, false, 0, address(0)); @@ -262,8 +262,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 150_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 516_666664); assertEq(_wrappedMToken.totalSupply(), 666_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 116_666664); - assertEq(_wrappedMToken.excess(), 416_666672); + assertEq(_wrappedMToken.totalAccruedYield(), 116_666672); + assertEq(_wrappedMToken.excess(), 416_666664); _earnerManager.setEarnerDetails(_carol, true, 0, address(0)); @@ -277,8 +277,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 350_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 316_666664); assertEq(_wrappedMToken.totalSupply(), 666_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 116_666664); - assertEq(_wrappedMToken.excess(), 416_666672); + assertEq(_wrappedMToken.totalAccruedYield(), 116_666672); + assertEq(_wrappedMToken.excess(), 416_666664); _mToken.setCurrentIndex(5 * _EXP_SCALED_ONE); _mToken.setBalanceOf(address(_wrappedMToken), 1_500_000000); // was 1200 @ 4.0, so 1500 @ 5.0 @@ -303,8 +303,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 350_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 316_666664); assertEq(_wrappedMToken.totalSupply(), 666_666664); - assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); - assertEq(_wrappedMToken.excess(), 600_000006); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_alice); _wrappedMToken.unwrap(_alice, 266_666664); @@ -317,8 +317,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 350_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 400_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); - assertEq(_wrappedMToken.excess(), 600_000006); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_bob); _wrappedMToken.unwrap(_bob, 150_000000); @@ -331,8 +331,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 250_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); - assertEq(_wrappedMToken.excess(), 600_000006); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_carol); _wrappedMToken.unwrap(_carol, 200_000000); @@ -345,8 +345,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000); assertEq(_wrappedMToken.totalSupply(), 50_000000); - assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); - assertEq(_wrappedMToken.excess(), 600_000006); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_dave); _wrappedMToken.unwrap(_dave, 50_000000); @@ -359,8 +359,8 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalSupply(), 0); - assertEq(_wrappedMToken.totalAccruedYield(), 233_333330); - assertEq(_wrappedMToken.excess(), 600_000006); + assertEq(_wrappedMToken.totalAccruedYield(), 233_333340); + assertEq(_wrappedMToken.excess(), 599_999996); } function test_noExcessCreep() external { @@ -397,8 +397,9 @@ contract StoryTests is Test { int256(_mToken.balanceOf(address(_wrappedMToken))) ); + uint256 bobBalance_ = _wrappedMToken.balanceOf(_bob); vm.prank(_bob); - _wrappedMToken.unwrap(_bob); + _wrappedMToken.unwrap(_bob, bobBalance_); } function test_dustWrapping() external { diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index a94169c..eb67573 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.26; +import { console2 } from "../../lib/forge-std/src/Test.sol"; + import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; import { UIntMath } from "../../lib/common/src/libs/UIntMath.sol"; @@ -159,7 +161,7 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 1_000); - assertEq(_wrappedMToken.internalWrap(_alice, _alice, 1_000), 1_000); + _wrappedMToken.internalWrap(_alice, _alice, 1_000); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 2_000); @@ -192,42 +194,42 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 999); - assertEq(_wrappedMToken.internalWrap(_alice, _alice, 999), 999); + _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 + 908); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 909); // round up total earning principal assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999); - assertEq(_wrappedMToken.totalAccruedYield(), 100); + assertEq(_wrappedMToken.totalAccruedYield(), 101); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 1); - assertEq(_wrappedMToken.internalWrap(_alice, _alice, 1), 1); + _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 + 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908 + 2); assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999 + 1); - assertEq(_wrappedMToken.totalAccruedYield(), 99); + assertEq(_wrappedMToken.totalAccruedYield(), 101); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 2); - assertEq(_wrappedMToken.internalWrap(_alice, _alice, 2), 2); + _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 + 1); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 + 908 + 0 + 4); assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 999 + 1 + 2); - assertEq(_wrappedMToken.totalAccruedYield(), 98); + assertEq(_wrappedMToken.totalAccruedYield(), 102); } /* ============ wrap ============ */ @@ -262,7 +264,7 @@ contract WrappedMTokenTests is Test { _getMaxAmount(_wrappedMToken.currentIndex()) ); - _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); + _setupAccount(_alice, earningEnabled_ && accountEarning_, balanceWithYield_, balance_); wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(_wrappedMToken.currentIndex()) - balanceWithYield_)); @@ -283,7 +285,9 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); assertEq( - accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), + earningEnabled_ && accountEarning_ + ? _wrappedMToken.totalEarningSupply() + : _wrappedMToken.totalNonEarningSupply(), _wrappedMToken.balanceOf(_alice) ); } @@ -298,56 +302,6 @@ contract WrappedMTokenTests is Test { _wrappedMToken.wrap(_alice, uint256(type(uint240).max) + 1); } - function testFuzz_wrap_entireBalance( - 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, 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.startPrank(_alice); - _wrappedMToken.wrap(_alice); - - if (wrapAmount_ == 0) return; - - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); - - assertEq( - accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), - _wrappedMToken.balanceOf(_alice) - ); - } - /* ============ wrapWithPermit vrs ============ */ function test_wrapWithPermit_vrs_invalidAmount() external { vm.expectRevert(UIntMath.InvalidUInt240.selector); @@ -510,7 +464,7 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 1); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 1); + _wrappedMToken.internalUnwrap(_alice, _alice, 1); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 999); @@ -523,7 +477,7 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 499); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 499), 499); + _wrappedMToken.internalUnwrap(_alice, _alice, 499); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 500); @@ -536,7 +490,7 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 500); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 500); + _wrappedMToken.internalUnwrap(_alice, _alice, 500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 0); @@ -570,42 +524,42 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 1); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 1), 1); + _wrappedMToken.internalUnwrap(_alice, _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 - 1); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000); assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1); - assertEq(_wrappedMToken.totalAccruedYield(), 100); + assertEq(_wrappedMToken.totalAccruedYield(), 101); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 499); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 499), 499); + _wrappedMToken.internalUnwrap(_alice, _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); + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 - 1 - 454 + 2); assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1 - 499); - assertEq(_wrappedMToken.totalAccruedYield(), 100); + assertEq(_wrappedMToken.totalAccruedYield(), 102); vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 500); - assertEq(_wrappedMToken.internalUnwrap(_alice, _alice, 500), 500); + _wrappedMToken.internalUnwrap(_alice, _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); // 0 + assertEq(_wrappedMToken.totalEarningPrincipal(), 1_000 - 1 - 454 - 455 + 3); assertEq(_wrappedMToken.totalEarningSupply(), 1_000 - 1 - 499 - 500); // 0 - assertEq(_wrappedMToken.totalAccruedYield(), 99); + assertEq(_wrappedMToken.totalAccruedYield(), 103); } /* ============ unwrap ============ */ @@ -670,51 +624,6 @@ contract WrappedMTokenTests is Test { ); } - /* ============ unwrap entire balance ============ */ - function testFuzz_unwrap_entireBalance( - bool earningEnabled_, - bool accountEarning_, - 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, accountEarning_, balanceWithYield_, balance_); - - _mToken.setBalanceOf(address(_wrappedMToken), balance_); - - if (balance_ == 0) { - vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, (0))); - } else { - vm.expectEmit(); - emit IERC20.Transfer(_alice, address(0), balance_); - } - - vm.startPrank(_alice); - _wrappedMToken.unwrap(_alice); - - if (balance_ == 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); @@ -788,9 +697,9 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_bob), 100); assertEq(_wrappedMToken.totalNonEarningSupply(), 100); - assertEq(_wrappedMToken.totalEarningPrincipal(), 909); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalAccruedYield(), 0); + assertEq(_wrappedMToken.totalEarningPrincipal(), 910); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); // round up total earning principal + assertEq(_wrappedMToken.totalAccruedYield(), 1); } function test_claimFor_earner_withFee() external { @@ -986,23 +895,21 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); - uint240 earmarked_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; - int248 excess_ = earmarked_ <= 0 - ? int248(uint248(mBalance_)) - : int248(uint248(mBalance_)) - int248(uint248(earmarked_)); + uint240 earmarked_ = totalNonEarningSupply_ + _wrappedMToken.projectedEarningSupply(); + int240 excess_ = int240(mBalance_) - int240(earmarked_); if (excess_ <= 0) { vm.expectRevert(IWrappedMToken.NoExcess.selector); } else { vm.expectEmit(false, false, false, false); - emit IWrappedMToken.ExcessClaimed(uint240(uint248(excess_))); + emit IWrappedMToken.ExcessClaimed(uint240(excess_)); } uint240 claimed_ = _wrappedMToken.claimExcess(); if (excess_ <= 0) return; - assertLe(claimed_, uint248(excess_)); + assertLe(claimed_, uint240(excess_)); } /* ============ transfer ============ */ @@ -1119,9 +1026,9 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_bob), 1_000); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalEarningPrincipal(), 545); + assertEq(_wrappedMToken.totalEarningPrincipal(), 546); assertEq(_wrappedMToken.totalEarningSupply(), 500); - assertEq(_wrappedMToken.totalAccruedYield(), 100); + assertEq(_wrappedMToken.totalAccruedYield(), 101); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 1); @@ -1136,9 +1043,9 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_bob), 1_001); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_001); - assertEq(_wrappedMToken.totalEarningPrincipal(), 544); + assertEq(_wrappedMToken.totalEarningPrincipal(), 546); assertEq(_wrappedMToken.totalEarningSupply(), 499); - assertEq(_wrappedMToken.totalAccruedYield(), 100); + assertEq(_wrappedMToken.totalAccruedYield(), 102); } function test_transfer_fromNonEarner_toEarner() external { @@ -1168,9 +1075,9 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_bob), 49); assertEq(_wrappedMToken.totalNonEarningSupply(), 500); - assertEq(_wrappedMToken.totalEarningPrincipal(), 954); + assertEq(_wrappedMToken.totalEarningPrincipal(), 955); assertEq(_wrappedMToken.totalEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalAccruedYield(), 50); + assertEq(_wrappedMToken.totalAccruedYield(), 51); } function test_transfer_fromEarner_toEarner() external { @@ -1320,10 +1227,18 @@ contract WrappedMTokenTests is Test { /* ============ startEarningFor ============ */ 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_startEarning_overflow() external { _mToken.setCurrentIndex(1_100000000000); _wrappedMToken.setEnableMIndex(1_100000000000); @@ -1360,12 +1275,11 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1000); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningPrincipal(), 909); + assertEq(_wrappedMToken.totalEarningPrincipal(), 910); assertEq(_wrappedMToken.totalEarningSupply(), 1_000); } function testFuzz_startEarningFor( - bool earningEnabled_, uint240 balance_, uint128 currentMIndex_, uint128 enableMIndex_, @@ -1377,7 +1291,7 @@ contract WrappedMTokenTests is Test { disableIndex_ ); - _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + _setupIndexes(true, currentMIndex_, enableMIndex_, disableIndex_); uint128 currentIndex_ = _wrappedMToken.currentIndex(); @@ -1392,15 +1306,16 @@ contract WrappedMTokenTests is Test { _wrappedMToken.startEarningFor(_alice); - uint112 earningPrincipal_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); + uint112 principalDown_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); + uint112 principalUp_ = IndexingMath.getPrincipalAmountRoundedUp(balance_, currentIndex_); assertEq(_wrappedMToken.isEarning(_alice), true); - assertEq(_wrappedMToken.earningPrincipalOf(_alice), earningPrincipal_); + assertEq(_wrappedMToken.earningPrincipalOf(_alice), principalDown_); assertEq(_wrappedMToken.balanceOf(_alice), balance_); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningSupply(), balance_); - assertEq(_wrappedMToken.totalEarningPrincipal(), earningPrincipal_); + assertEq(_wrappedMToken.totalEarningPrincipal(), principalUp_); } /* ============ startEarningFor batch ============ */ diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index 2c7459f..5e35449 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -13,16 +13,12 @@ contract WrappedMTokenHarness is WrappedMToken { address migrationAdmin_ ) WrappedMToken(mToken_, registrar_, earnerManager_, excessDestination_, migrationAdmin_) {} - function internalWrap(address account_, address recipient_, uint240 amount_) external returns (uint240 wrapped_) { - return _wrap(account_, recipient_, amount_); + function internalWrap(address account_, address recipient_, uint240 amount_) external { + _wrap(account_, recipient_, amount_); } - function internalUnwrap( - address account_, - address recipient_, - uint240 amount_ - ) external returns (uint240 unwrapped_) { - return _unwrap(account_, recipient_, amount_); + function internalUnwrap(address account_, address recipient_, uint240 amount_) external { + _unwrap(account_, recipient_, amount_); } function setIsEarningOf(address account_, bool isEarning_) external { From 5851babb6e72a5f55e5c8869ce5e11c1f39062b1 Mon Sep 17 00:00:00 2001 From: toninorair Date: Sun, 16 Mar 2025 21:10:01 -0400 Subject: [PATCH 12/27] Excess fixes after reviews (#119) * Increase excess type * Ignore negative rounding error --- src/WrappedMToken.sol | 17 +++++++++-------- src/interfaces/IWrappedMToken.sol | 4 ++-- test/integration/UniswapV3.t.sol | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index c74cc36..1674101 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -102,7 +102,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { uint128 public disableIndex; /// @inheritdoc IWrappedMToken - int240 public roundingError; + int256 public roundingError; mapping(address account => address claimRecipient) internal _claimRecipients; @@ -171,11 +171,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function claimExcess() external returns (uint240 claimed_) { - int240 excess_ = excess(); + int256 excess_ = excess(); if (excess_ <= 0) revert NoExcess(); - emit ExcessClaimed(claimed_ = uint240(excess_)); + emit ExcessClaimed(claimed_ = uint240(uint256(excess_))); // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. IMTokenLike(mToken).transfer(excessDestination, claimed_); @@ -314,13 +314,14 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /// @inheritdoc IWrappedMToken - function excess() public view returns (int240 excess_) { + function excess() public view returns (int256 excess_) { unchecked { - uint240 earmarked_ = totalNonEarningSupply + projectedEarningSupply(); - uint240 balance_ = _mBalanceOf(address(this)); + uint256 earmarked_ = totalNonEarningSupply + projectedEarningSupply(); + uint256 balance_ = _mBalanceOf(address(this)); + int256 roundingError_ = roundingError > 0 ? roundingError : int256(0); - // Decreases claimable excess by the `roundingError` for extra level of safety and solvency. - return int240(balance_) - int240(earmarked_) - roundingError; + // Reduces claimable excess if roundingError is positive, adding an extra layer of safety and solvency. + return int256(balance_) - int256(earmarked_) - roundingError_; } } diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 6d9a11e..a0ce58c 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -258,7 +258,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { 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 (int240 excess); + 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); @@ -304,5 +304,5 @@ interface IWrappedMToken is IMigratable, IERC20Extended { function excessDestination() external view returns (address excessDestination); /// @notice The rounding error that may occur due to imprecise $M transfers in and out of WrappedM contract. - function roundingError() external view returns (int240 roundingError); + function roundingError() external view returns (int256 roundingError); } diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index 07a487a..6cfd256 100644 --- a/test/integration/UniswapV3.t.sol +++ b/test/integration/UniswapV3.t.sol @@ -50,7 +50,7 @@ contract UniswapV3IntegrationTests is TestBase { uint256 internal _poolAccruedYield; uint256 internal _bobAccruedYield; - int248 internal _excess; + int256 internal _excess; function setUp() external { _deployV2Components(); From c15bc3043258a44e63bab1a5e240cec5397e1701 Mon Sep 17 00:00:00 2001 From: toninorair Date: Thu, 3 Apr 2025 21:26:00 -0400 Subject: [PATCH 13/27] Chainsecurity review fixes (#121) --- src/EarnerManager.sol | 5 +- src/WrappedMToken.sol | 41 +++-------- src/interfaces/IWrappedMToken.sol | 3 - test/integration/Protocol.t.sol | 117 ++---------------------------- 4 files changed, 20 insertions(+), 146 deletions(-) diff --git a/src/EarnerManager.sol b/src/EarnerManager.sol index 3859a07..04d0e91 100644 --- a/src/EarnerManager.sol +++ b/src/EarnerManager.sol @@ -150,7 +150,7 @@ contract EarnerManager is IEarnerManager, Migratable { function getEarnerDetails(address account_) external view returns (bool status_, uint16 feeRate_, address admin_) { if (earnersListsIgnored() || isInRegistrarEarnersList(account_)) return (true, 0, address(0)); - EarnerDetails storage details_ = _earnerDetails[account_]; + EarnerDetails memory details_ = _earnerDetails[account_]; // NOTE: Not using `isInAdministratedEarnersList(account_)` here to avoid redundant storage reads. return _isValidAdmin(details_.admin) ? (true, details_.feeRate, details_.admin) : (false, 0, address(0)); @@ -179,7 +179,7 @@ contract EarnerManager is IEarnerManager, Migratable { continue; } - EarnerDetails storage details_ = _earnerDetails[account_]; + EarnerDetails memory details_ = _earnerDetails[account_]; // NOTE: Not using `isInAdministratedEarnersList(account_)` here to avoid redundant storage reads. if (!_isValidAdmin(details_.admin)) continue; @@ -192,7 +192,6 @@ contract EarnerManager is IEarnerManager, Migratable { /// @inheritdoc IEarnerManager function isAdmin(address account_) public view returns (bool isAdmin_) { - // TODO: Consider transient storage for memoizing this check. return IRegistrarLike(registrar).listContains(ADMINS_LIST_NAME, account_); } diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 1674101..fcb5fa9 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -101,9 +101,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken uint128 public disableIndex; - /// @inheritdoc IWrappedMToken - int256 public roundingError; - mapping(address account => address claimRecipient) internal _claimRecipients; /* ============ Constructor ============ */ @@ -270,6 +267,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function balanceWithYieldOf(address account_) external view returns (uint256 balance_) { + // NOTE: The returned amount includes the total accrued yield, regardless of whether it is split between the claim recipient and the earner manager. + // 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_` and may be split between the earner manager and the `account_`. unchecked { return balanceOf(account_) + accruedYieldOf(account_); } @@ -318,10 +318,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { unchecked { uint256 earmarked_ = totalNonEarningSupply + projectedEarningSupply(); uint256 balance_ = _mBalanceOf(address(this)); - int256 roundingError_ = roundingError > 0 ? roundingError : int256(0); - // Reduces claimable excess if roundingError is positive, adding an extra layer of safety and solvency. - return int256(balance_) - int256(earmarked_) - roundingError_; + return int256(balance_) - int256(earmarked_); } } @@ -669,24 +667,14 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param amount_ The amount of M deposited. */ function _wrap(address account_, address recipient_, uint240 amount_) internal { - uint240 startingBalance_ = _mBalanceOf(address(this)); - // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. IMTokenLike(mToken).transferFrom(account_, address(this), amount_); - // NOTE: Computes the actual increase in the $M balance of the `WrappedM` contract and tracks potential $M rounding adjustments. - // Option 1: $M transfer from an $M earner to another $M earner (`WrappedM` in earning state) → rounds up → rounds up, + // 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. - // Result: `roundingError` remains the same or decreases. - // - // 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 deducted from $M locked in `WrappedM` compared to the minted amount of Wrapped $M. - // Result: `roundingError` remains the same or increases. - uint240 endingBalance_ = _mBalanceOf(address(this)); - uint240 mIncrease_ = endingBalance_ - startingBalance_; - roundingError += int240(amount_) - int240(mIncrease_); - - // Mints precise amount of Wrapped $M to `recipient_`. + // 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_); } @@ -699,19 +687,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function _unwrap(address account_, address recipient_, uint240 amount_) internal { _burn(account_, amount_); - uint240 startingBalance_ = _mBalanceOf(address(this)); - // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. - IMTokenLike(mToken).transfer(recipient_, amount_); - // 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 → rounds up. - // Option 2: $M transfer from an $M earner (`WrappedM` in earning state) to an $M non-earner → precise $M transfer. + // 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. - // Result: `roundingError` remains the same or increases. - uint240 endingBalance_ = _mBalanceOf(address(this)); - uint240 mDecrease_ = startingBalance_ - endingBalance_; - roundingError += int240(mDecrease_) - int240(amount_); + IMTokenLike(mToken).transfer(recipient_, amount_); } /** diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index a0ce58c..30a19c9 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -302,7 +302,4 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the destination where excess is claimed to. function excessDestination() external view returns (address excessDestination); - - /// @notice The rounding error that may occur due to imprecise $M transfers in and out of WrappedM contract. - function roundingError() external view returns (int256 roundingError); } diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index bc67d27..6870eb6 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -99,8 +99,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); - assertEq(_wrappedMToken.roundingError(), 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( int256(_wrapperBalanceOfM), @@ -125,7 +124,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 50_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); - assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -177,8 +175,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); - assertEq(_wrappedMToken.roundingError(), 2); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( int256(_wrapperBalanceOfM), @@ -203,8 +200,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 150_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); - assertEq(_wrappedMToken.roundingError(), 3); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( int256(_wrapperBalanceOfM), @@ -226,7 +222,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1_190592); assertEq(_wrappedMToken.excess(), _excess); - assertEq(_wrappedMToken.roundingError(), 3); assertGe( int256(_wrapperBalanceOfM), @@ -292,8 +287,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); - assertEq(_wrappedMToken.roundingError(), 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( int256(_wrapperBalanceOfM), @@ -344,7 +338,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + _aliceBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); assertEq(_wrappedMToken.excess(), _excess -= 1); - assertEq(_wrappedMToken.roundingError(), 1); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += _aliceBalance); @@ -374,7 +367,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 50_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); assertEq(_wrappedMToken.excess(), _excess -= 1); - assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -433,8 +425,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); - assertEq(_wrappedMToken.roundingError(), 1); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( int256(_wrapperBalanceOfM), @@ -467,7 +458,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); assertEq(_wrappedMToken.excess(), _excess -= 1); - assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -500,7 +490,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614473 + 1); assertEq(_wrappedMToken.excess(), _excess += 1); - assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -521,7 +510,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _carolBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); assertEq(_wrappedMToken.excess(), _excess -= 1); - assertEq(_wrappedMToken.roundingError(), 1); assertGe( int256(_wrapperBalanceOfM), @@ -551,7 +539,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _aliceBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess); - assertEq(_wrappedMToken.roundingError(), 1); // Assert Alice (Non-Earner) assertEq(_mToken.balanceOf(_alice), _aliceBalance); @@ -570,8 +557,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _bobBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); - assertEq(_wrappedMToken.excess(), _excess -= 3); - assertEq(_wrappedMToken.roundingError(), 2); + assertEq(_wrappedMToken.excess(), _excess -= 2); // Assert Bob (Earner) assertEq(_mToken.balanceOf(_bob), _bobBalance); @@ -591,7 +577,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); assertEq(_wrappedMToken.excess(), _excess -= 1); - assertEq(_wrappedMToken.roundingError(), 2); // Assert Carol (Earner) assertEq(_mToken.balanceOf(_carol), _carolBalance); @@ -609,8 +594,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); - assertEq(_wrappedMToken.roundingError(), 3); + assertEq(_wrappedMToken.excess(), _excess -= 1); // Assert Dave (Non-Earner) assertEq(_mToken.balanceOf(_dave), _daveBalance); @@ -633,7 +617,6 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); assertEq(_wrappedMToken.excess(), _excess -= _excess); - assertEq(_wrappedMToken.roundingError(), 3); assertGe( int256(_wrapperBalanceOfM), @@ -776,92 +759,6 @@ contract ProtocolIntegrationTests is TestBase { } } - function test_integration_roundingError() external { - // Alice is $M earner, bob is not - vm.prank(_alice); - _mToken.startEarning(); - - _giveM(_alice, 100_000000); - _giveM(_bob, 100_000000); - - // Wraps for earners and non-earners with different values of $M index (timestamps) - uint256 mBalanceBeforeWrap_ = _mToken.balanceOf(address(_wrappedMToken)); - _wrap(_alice, _alice, 10_000000); - uint256 mBalanceAfterWrap_ = _mToken.balanceOf(address(_wrappedMToken)); - - uint256 mDelta = mBalanceAfterWrap_ - mBalanceBeforeWrap_; - - assertEq(mDelta, 10000001); - assertEq(_wrappedMToken.roundingError(), -1); - - mBalanceBeforeWrap_ = _mToken.balanceOf(address(_wrappedMToken)); - _wrap(_bob, _bob, 10_000000); - mBalanceAfterWrap_ = _mToken.balanceOf(address(_wrappedMToken)); - - mDelta = mBalanceAfterWrap_ - mBalanceBeforeWrap_; - - assertEq(mDelta, 10000000); - assertEq(_wrappedMToken.roundingError(), -1); - - vm.warp(vm.getBlockTimestamp() + 5 seconds); - - mBalanceBeforeWrap_ = _mToken.balanceOf(address(_wrappedMToken)); - _wrap(_bob, _bob, 10_000000); - mBalanceAfterWrap_ = _mToken.balanceOf(address(_wrappedMToken)); - - mDelta = mBalanceAfterWrap_ - mBalanceBeforeWrap_; - - assertEq(mDelta, 9999999); - assertEq(_wrappedMToken.roundingError(), 0); - - mBalanceBeforeWrap_ = _mToken.balanceOf(address(_wrappedMToken)); - _wrap(_alice, _alice, 10_000000); - mBalanceAfterWrap_ = _mToken.balanceOf(address(_wrappedMToken)); - - mDelta = mBalanceAfterWrap_ - mBalanceBeforeWrap_; - - assertEq(mDelta, 10000000); - assertEq(_wrappedMToken.roundingError(), 0); - - // Unwraps - - uint256 mBalanceBeforeUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); - _unwrap(_alice, _alice, 10_000000); - uint256 mBalanceAfterUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); - mDelta = mBalanceBeforeUnwrap_ - mBalanceAfterUnwrap_; - - assertEq(mDelta, 10000000); - assertEq(_wrappedMToken.roundingError(), 0); - - mBalanceBeforeUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); - _unwrap(_bob, _bob, 10_000000); - mBalanceAfterUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); - mDelta = mBalanceBeforeUnwrap_ - mBalanceAfterUnwrap_; - - assertEq(mDelta, 10_000000); - assertEq(_wrappedMToken.roundingError(), 0); - - vm.warp(vm.getBlockTimestamp() + 10 minutes); - - mBalanceBeforeUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); - _unwrap(_alice, _alice, 10_000000); - mBalanceAfterUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); - mDelta = mBalanceBeforeUnwrap_ - mBalanceAfterUnwrap_; - - assertEq(mDelta, 10_000001); - assertEq(_wrappedMToken.roundingError(), 1); - - vm.warp(vm.getBlockTimestamp() + 90 minutes); - - mBalanceBeforeUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); - _unwrap(_bob, _bob, 10_000000); - mBalanceAfterUnwrap_ = _mToken.balanceOf(address(_wrappedMToken)); - mDelta = mBalanceBeforeUnwrap_ - mBalanceAfterUnwrap_; - - assertEq(mDelta, 10_000001); - assertEq(_wrappedMToken.roundingError(), 2); - } - function _getNewSeed(uint256 seed_) internal pure returns (uint256 newSeed_) { return uint256(keccak256(abi.encodePacked(seed_))); } From f79af77c7aa5d3337b2fa3e2d3910c7213824c36 Mon Sep 17 00:00:00 2001 From: toninorair Date: Tue, 8 Apr 2025 16:38:48 -0400 Subject: [PATCH 14/27] Return vs revert if excess == 0 (#122) --- src/WrappedMToken.sol | 2 +- src/interfaces/IWrappedMToken.sol | 3 --- test/unit/WrappedMToken.t.sol | 12 ++++++------ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index fcb5fa9..87fc240 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -170,7 +170,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function claimExcess() external returns (uint240 claimed_) { int256 excess_ = excess(); - if (excess_ <= 0) revert NoExcess(); + if (excess_ <= 0) return 0; emit ExcessClaimed(claimed_ = uint240(uint256(excess_))); diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 30a19c9..b6fce77 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -85,9 +85,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ error NotApprovedEarner(address account); - /// @notice Emitted when there is no excess to claim. - error NoExcess(); - /// @notice Emitted when the non-governance migrate function is called by an account other than the migration admin. error UnauthorizedMigration(); diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index eb67573..32e1351 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -898,18 +898,18 @@ contract WrappedMTokenTests is Test { uint240 earmarked_ = totalNonEarningSupply_ + _wrappedMToken.projectedEarningSupply(); int240 excess_ = int240(mBalance_) - int240(earmarked_); - if (excess_ <= 0) { - vm.expectRevert(IWrappedMToken.NoExcess.selector); - } else { + if (excess_ > 0) { vm.expectEmit(false, false, false, false); emit IWrappedMToken.ExcessClaimed(uint240(excess_)); } uint240 claimed_ = _wrappedMToken.claimExcess(); - if (excess_ <= 0) return; - - assertLe(claimed_, uint240(excess_)); + if (excess_ <= 0) { + assertEq(claimed_, 0); + } else { + assertLe(claimed_, uint240(excess_)); + } } /* ============ transfer ============ */ From 296a42b066c719c2be77b64cc80ff50d25f5724f Mon Sep 17 00:00:00 2001 From: 0xIryna <43921510+0xIryna@users.noreply.github.com> Date: Tue, 16 Dec 2025 01:34:31 -0800 Subject: [PATCH 15/27] feat: use SwapFacility (#123) * feat: use SwapFacility * chore: rename M^0 -> M0 * fix: tests --- .env.deploy.example | 1 + script/Deploy.s.sol | 1 + script/DeployBase.sol | 8 +- script/DeployUpgradeMainnet.s.sol | 4 + src/WrappedMToken.sol | 64 ++++---- src/interfaces/ISwapFacilityLike.sol | 25 +++ src/interfaces/IWrappedMToken.sol | 38 ++--- test/integration/Deploy.t.sol | 5 + test/integration/Protocol.t.sol | 18 +-- test/integration/TestBase.sol | 52 +++---- test/integration/Upgrade.t.sol | 5 + test/unit/Migrations.t.sol | 3 + test/unit/Stories.t.sol | 43 ++++-- test/unit/WrappedMToken.t.sol | 223 ++++++++++----------------- test/utils/Mocks.sol | 35 +++++ test/utils/WrappedMTokenHarness.sol | 11 +- 16 files changed, 264 insertions(+), 272 deletions(-) create mode 100644 src/interfaces/ISwapFacilityLike.sol diff --git a/.env.deploy.example b/.env.deploy.example index 3453c66..d19c4cf 100644 --- a/.env.deploy.example +++ b/.env.deploy.example @@ -6,6 +6,7 @@ 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 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 5e373f6..9a6403f 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -70,6 +70,7 @@ contract DeployProduction is Script, DeployBase { vm.envAddress("M_TOKEN"), vm.envAddress("REGISTRAR"), vm.envAddress("EXCESS_DESTINATION"), + vm.envAddress("SWAP_FACILITY"), vm.envAddress("WRAPPED_M_MIGRATION_ADMIN"), vm.envAddress("EARNER_MANAGER_MIGRATION_ADMIN") ); diff --git a/script/DeployBase.sol b/script/DeployBase.sol index a68ddb9..6313927 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -14,6 +14,7 @@ contract DeployBase { * @dev Deploys Wrapped M Token. * @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 excessDestination_ The address of the excess destination. * @param wrappedMMigrationAdmin_ The address of the Wrapped M Migration Admin. * @param earnerManagerMigrationAdmin_ The address of the Earner Manager Migration Admin. @@ -26,6 +27,7 @@ contract DeployBase { address mToken_, address registrar_, address excessDestination_, + address swapFacility_, address wrappedMMigrationAdmin_, address earnerManagerMigrationAdmin_ ) @@ -48,7 +50,7 @@ contract DeployBase { earnerManagerProxy_ = address(new Proxy(earnerManagerImplementation_)); wrappedMTokenImplementation_ = address( - new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, wrappedMMigrationAdmin_) + new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) ); wrappedMTokenProxy_ = address(new Proxy(wrappedMTokenImplementation_)); @@ -59,6 +61,7 @@ contract DeployBase { * @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 earnerManagerMigrationAdmin_ The address of the Earner Manager Migration Admin. * @return earnerManagerImplementation_ The address of the deployed Earner Manager implementation. @@ -70,6 +73,7 @@ contract DeployBase { address mToken_, address registrar_, address excessDestination_, + address swapFacility_, address wrappedMMigrationAdmin_, address earnerManagerMigrationAdmin_, address[] memory earners_ @@ -93,7 +97,7 @@ contract DeployBase { earnerManagerProxy_ = address(new Proxy(earnerManagerImplementation_)); wrappedMTokenImplementation_ = address( - new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, wrappedMMigrationAdmin_) + new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) ); wrappedMTokenMigrator_ = address(new WrappedMTokenMigratorV1(wrappedMTokenImplementation_, earners_)); diff --git a/script/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol index cbec01c..271b8cc 100644 --- a/script/DeployUpgradeMainnet.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -22,6 +22,9 @@ contract DeployUpgradeMainnet is Script, DeployBase { // NOTE: Ensure this is the correct Excess Destination mainnet address. address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault + // TODO: Replace with the correct Swap Facility mainnet address after deployment. + address internal constant _SWAP_FACILITY = address(0); + address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; // NOTE: Ensure this is the correct Migration Admin mainnet address. @@ -137,6 +140,7 @@ contract DeployUpgradeMainnet is Script, DeployBase { _M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, + _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, _EARNER_MANAGER_MIGRATION_ADMIN, earners_ diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 87fc240..37fe39e 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -28,7 +28,7 @@ import { IWrappedMToken } from "./interfaces/IWrappedMToken.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 { /* ============ Structs ============ */ @@ -83,6 +83,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken address public immutable excessDestination; + /// @inheritdoc IWrappedMToken + address public immutable swapFacility; + /// @inheritdoc IWrappedMToken uint112 public totalEarningPrincipal; @@ -103,6 +106,14 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { mapping(address account => address claimRecipient) internal _claimRecipients; + /* ============ Modifiers ============ */ + + /// @dev Modifier to check if caller is SwapFacility. + modifier onlySwapFacility() { + if (msg.sender != swapFacility) revert NotSwapFacility(); + _; + } + /* ============ Constructor ============ */ /** @@ -112,6 +123,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param registrar_ The address of a Registrar. * @param earnerManager_ The address of an Earner Manager. * @param excessDestination_ The address of an excess destination. + * @param swapFacility_ The address of a Swap Facility. * @param migrationAdmin_ The address of a migration admin. */ constructor( @@ -119,46 +131,27 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { address registrar_, address earnerManager_, address excessDestination_, + address swapFacility_, address migrationAdmin_ - ) ERC20Extended("M (Wrapped) by M^0", "wM", 6) { + ) ERC20Extended("M (Wrapped) by M0", "wM", 6) { if ((mToken = mToken_) == address(0)) revert ZeroMToken(); if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); if ((earnerManager = earnerManager_) == address(0)) revert ZeroEarnerManager(); if ((excessDestination = excessDestination_) == address(0)) revert ZeroExcessDestination(); + if ((swapFacility = swapFacility_) == address(0)) revert ZeroSwapFacility(); if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); } /* ============ Interactive Functions ============ */ /// @inheritdoc IWrappedMToken - function wrap(address recipient_, uint256 amount_) external { - _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); - } - - /// @inheritdoc IWrappedMToken - function wrapWithPermit( - address recipient_, - uint256 amount_, - uint256 deadline_, - uint8 v_, - bytes32 r_, - bytes32 s_ - ) external { - try IMTokenLike(mToken).permit(msg.sender, address(this), amount_, deadline_, v_, r_, s_) {} catch {} - - _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); - } - - /// @inheritdoc IWrappedMToken - function wrapWithPermit(address recipient_, uint256 amount_, uint256 deadline_, bytes memory signature_) external { - try IMTokenLike(mToken).permit(msg.sender, address(this), amount_, deadline_, signature_) {} catch {} - - _wrap(msg.sender, recipient_, UIntMath.safe240(amount_)); + function wrap(address recipient_, uint256 amount_) external onlySwapFacility { + _wrap(recipient_, UIntMath.safe240(amount_)); } /// @inheritdoc IWrappedMToken - function unwrap(address recipient_, uint256 amount_) external { - _unwrap(msg.sender, recipient_, UIntMath.safe240(amount_)); + function unwrap(address /* recipient_ */, uint256 amount_) external onlySwapFacility { + _unwrap(UIntMath.safe240(amount_)); } /// @inheritdoc IWrappedMToken @@ -662,13 +655,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /** * @dev Wraps `amount` M from `account_` into wM for `recipient`. - * @param account_ The account from which M is deposited. * @param recipient_ The account receiving the minted wM. * @param amount_ The amount of M deposited. */ - function _wrap(address account_, address recipient_, uint240 amount_) internal { + function _wrap(address recipient_, uint240 amount_) internal { + // 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: 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, @@ -679,20 +672,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /** - * @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. + * @dev Unwraps `amount` wM from `account_` into M. * @param amount_ The amount of wM burned. */ - function _unwrap(address account_, address recipient_, uint240 amount_) internal { - _burn(account_, amount_); + function _unwrap(uint240 amount_) internal { + // 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. // 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(recipient_, amount_); + IMTokenLike(mToken).transfer(msg.sender, amount_); } /** diff --git a/src/interfaces/ISwapFacilityLike.sol b/src/interfaces/ISwapFacilityLike.sol new file mode 100644 index 0000000..8d8042e --- /dev/null +++ b/src/interfaces/ISwapFacilityLike.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.26; + +/** + * @title Subset of SwapFacility interface. + * @author M0 Labs + */ +interface ISwapFacilityLike { + /** + * @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; +} diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index b6fce77..496a008 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -103,44 +103,25 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @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 wM minted. */ function wrap(address recipient, uint256 amount) external; - /** - * @notice Wraps `amount` M from the caller into wM for `recipient`, using a permit. - * @param recipient The account receiving the minted wM. - * @param amount The amount of M deposited. - * @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 wrapWithPermit( - address recipient, - uint256 amount, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @notice Wraps `amount` M from the caller into wM for `recipient`, using a permit. - * @param recipient The account receiving the minted wM. - * @param amount The amount of M deposited. - * @param deadline The last timestamp where the signature is still valid. - * @param signature An arbitrary signature (EIP-712). - */ - function wrapWithPermit(address recipient, uint256 amount, uint256 deadline, bytes memory signature) external; - /** * @notice Unwraps `amount` wM from the caller into M for `recipient`. + * @dev Can only be called by the SwapFacility. * @param recipient The account receiving the withdrawn M. * @param amount The amount of wM burned. */ @@ -299,4 +280,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the destination where excess is claimed to. function excessDestination() external view returns (address excessDestination); + + /// @notice The address of the Swap Facility contract + function swapFacility() external view returns (address swapFacility); } diff --git a/test/integration/Deploy.t.sol b/test/integration/Deploy.t.sol index d8b3d57..700ff84 100644 --- a/test/integration/Deploy.t.sol +++ b/test/integration/Deploy.t.sol @@ -15,6 +15,8 @@ contract DeployTests is Test, DeployBase { address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EARNER_MANAGER_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault + // TODO: Replace with the actual Swap Facility address after deployment. + address internal constant _SWAP_FACILITY = 0x0000000000000000000000000000000000000001; address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; uint64 internal constant _DEPLOYER_NONCE = 50; @@ -39,6 +41,7 @@ contract DeployTests is Test, DeployBase { _M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, + _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, _EARNER_MANAGER_MIGRATION_ADMIN ); @@ -60,6 +63,7 @@ contract DeployTests is Test, DeployBase { assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); assertEq(IWrappedMToken(wrappedMTokenImplementation_).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).swapFacility(), _SWAP_FACILITY); // Wrapped M Token Proxy assertions assertEq(wrappedMTokenProxy_, expectedWrappedMTokenProxy_); @@ -68,6 +72,7 @@ contract DeployTests is Test, DeployBase { assertEq(IWrappedMToken(wrappedMTokenProxy_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenProxy_).registrar(), _REGISTRAR); assertEq(IWrappedMToken(wrappedMTokenProxy_).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(wrappedMTokenProxy_).swapFacility(), _SWAP_FACILITY); assertEq(IWrappedMToken(wrappedMTokenProxy_).implementation(), wrappedMTokenImplementation_); } } diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index 6870eb6..d72f2d0 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -54,7 +54,7 @@ contract ProtocolIntegrationTests is TestBase { 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 M^0"); + assertEq(_wrappedMToken.name(), "M (Wrapped) by M0"); assertEq(_wrappedMToken.symbol(), "wM"); assertEq(_wrappedMToken.decimals(), 6); } @@ -64,22 +64,6 @@ contract ProtocolIntegrationTests is TestBase { assertTrue(_mToken.isEarning(address(_wrappedMToken))); } - function test_wrapWithPermits() external { - _giveM(_alice, 200_000000); - - assertEq(_mToken.balanceOf(_alice), 200_000000); - - _wrapWithPermitVRS(_alice, _aliceKey, _alice, 100_000000, 0, block.timestamp); - - assertEq(_mToken.balanceOf(_alice), 100_000000); - assertEq(_wrappedMToken.balanceOf(_alice), 100_000000); - - _wrapWithPermitSignature(_alice, _aliceKey, _alice, 100_000000, 1, block.timestamp); - - assertEq(_mToken.balanceOf(_alice), 0); - assertEq(_wrappedMToken.balanceOf(_alice), 200_000000); - } - function test_integration_yieldAccumulation() external { _giveM(_alice, 100_000000); diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 33ca17a..b97a76d 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -10,6 +10,7 @@ import { Test } from "../../lib/forge-std/src/Test.sol"; import { Proxy } from "../../lib/common/src/Proxy.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; +import { ISwapFacilityLike } from "../../src/interfaces/ISwapFacilityLike.sol"; import { EarnerManager } from "../../src/EarnerManager.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; @@ -17,6 +18,8 @@ import { WrappedMTokenMigratorV1 } from "../../src/WrappedMTokenMigratorV1.sol"; import { IMTokenLike, IRegistrarLike } from "./vendor/protocol/Interfaces.sol"; +import { MockSwapFacility } from "../utils/Mocks.sol"; + contract TestBase is Test { IMTokenLike internal constant _mToken = IMTokenLike(0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b); @@ -68,6 +71,9 @@ contract TestBase is Test { address internal _wrappedMTokenImplementationV2; address internal _wrappedMTokenMigratorV1; + // TODO: Replace with the actual Swap Facility address after deployment. + address internal _swapFacility; + address[] internal _earners = [ 0x437cc33344a0B27A429f795ff6B469C72698B291, 0x0502d65f26f45d17503E4d34441F5e73Ea143033, @@ -145,43 +151,18 @@ contract TestBase is Test { 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 _wrapWithPermitVRS( - address account_, - uint256 signerPrivateKey_, - address recipient_, - uint256 amount_, - uint256 nonce_, - uint256 deadline_ - ) internal { - (uint8 v_, bytes32 r_, bytes32 s_) = _getPermit(account_, signerPrivateKey_, amount_, nonce_, deadline_); + _mToken.approve(_swapFacility, amount_); vm.prank(account_); - _wrappedMToken.wrapWithPermit(recipient_, amount_, deadline_, v_, r_, s_); + ISwapFacilityLike(_swapFacility).swapInM(address(_wrappedMToken), amount_, recipient_); } - function _wrapWithPermitSignature( - address account_, - uint256 signerPrivateKey_, - address recipient_, - uint256 amount_, - uint256 nonce_, - uint256 deadline_ - ) internal { - (uint8 v_, bytes32 r_, bytes32 s_) = _getPermit(account_, signerPrivateKey_, amount_, nonce_, deadline_); - + function _unwrap(address account_, address recipient_, uint256 amount_) internal { vm.prank(account_); - _wrappedMToken.wrapWithPermit(recipient_, amount_, deadline_, abi.encodePacked(r_, s_, v_)); - } + _wrappedMToken.approve(_swapFacility, amount_); - function _unwrap(address account_, address recipient_, uint256 amount_) internal { vm.prank(account_); - _wrappedMToken.unwrap(recipient_, amount_); + ISwapFacilityLike(_swapFacility).swapOutM(address(_wrappedMToken), amount_, recipient_); } function _transferWM(address sender_, address recipient_, uint256 amount_) internal { @@ -206,8 +187,17 @@ contract TestBase is Test { function _deployV2Components() internal { _earnerManagerImplementation = address(new EarnerManager(_registrar, _migrationAdmin)); _earnerManager = address(new Proxy(_earnerManagerImplementation)); + // TODO: Replace with the actual Swap Facility address after deployment. + _swapFacility = address(new MockSwapFacility(address(_mToken))); _wrappedMTokenImplementationV2 = address( - new WrappedMToken(address(_mToken), _registrar, _earnerManager, _excessDestination, _migrationAdmin) + new WrappedMToken( + address(_mToken), + _registrar, + _earnerManager, + _excessDestination, + _swapFacility, + _migrationAdmin + ) ); address[] memory earners_ = new address[](_earners.length); diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index f131b2e..7b44dfd 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -16,6 +16,8 @@ contract UpgradeTests is Test, DeployBase { address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EARNER_MANAGER_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault + // TODO: Replace with the actual Swap Facility address after deployment. + address internal constant _SWAP_FACILITY = 0x0000000000000000000000000000000000000001; address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; uint64 internal constant _DEPLOYER_NONCE = 50; @@ -84,6 +86,7 @@ contract UpgradeTests is Test, DeployBase { _M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, + _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, _EARNER_MANAGER_MIGRATION_ADMIN, earners_ @@ -106,6 +109,7 @@ contract UpgradeTests is Test, DeployBase { assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); assertEq(IWrappedMToken(wrappedMTokenImplementation_).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(wrappedMTokenImplementation_).swapFacility(), _SWAP_FACILITY); // Migrator assertions assertEq(wrappedMTokenMigrator_, expectedWrappedMTokenMigrator_); @@ -126,6 +130,7 @@ contract UpgradeTests is Test, DeployBase { assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).mToken(), _M_TOKEN); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).registrar(), _REGISTRAR); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).swapFacility(), _SWAP_FACILITY); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).implementation(), wrappedMTokenImplementation_); // Relevant storage slots. diff --git a/test/unit/Migrations.t.sol b/test/unit/Migrations.t.sol index b58c89e..b97711d 100644 --- a/test/unit/Migrations.t.sol +++ b/test/unit/Migrations.t.sol @@ -50,6 +50,7 @@ contract MigrationTests is Test { address internal _mToken = makeAddr("mToken"); address internal _earnerManager = makeAddr("earnerManager"); address internal _excessDestination = makeAddr("excessDestination"); + address internal _swapFacility = makeAddr("swapFacility"); address internal _migrationAdmin = makeAddr("migrationAdmin"); function test_wrappedMToken_migration() external { @@ -62,6 +63,7 @@ contract MigrationTests is Test { address(registrar_), _earnerManager, _excessDestination, + _swapFacility, _migrationAdmin ) ); @@ -89,6 +91,7 @@ contract MigrationTests is Test { address(registrar_), _earnerManager, _excessDestination, + _swapFacility, _migrationAdmin ) ); diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index acb6fa9..95e8c87 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -11,7 +11,7 @@ import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; -import { MockEarnerManager, MockM, MockRegistrar } from "../utils/Mocks.sol"; +import { MockEarnerManager, MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; contract StoryTests is Test { uint56 internal constant _EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; @@ -29,6 +29,7 @@ contract StoryTests is Test { MockEarnerManager internal _earnerManager; MockM internal _mToken; MockRegistrar internal _registrar; + MockSwapFacility internal _swapFacility; WrappedMToken internal _implementation; IWrappedMToken internal _wrappedMToken; @@ -37,6 +38,7 @@ contract StoryTests is Test { _mToken = new MockM(); _mToken.setCurrentIndex(_EXP_SCALED_ONE); + _swapFacility = new MockSwapFacility(address(_mToken)); _earnerManager = new MockEarnerManager(); @@ -45,6 +47,7 @@ contract StoryTests is Test { address(_registrar), address(_earnerManager), _excessDestination, + address(_swapFacility), _migrationAdmin ); @@ -65,7 +68,7 @@ contract StoryTests is Test { _mToken.setBalanceOf(_alice, 100_000000); vm.prank(_alice); - _wrappedMToken.wrap(_alice, 100_000000); + _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _alice); // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), 100_000000); @@ -81,7 +84,7 @@ contract StoryTests is Test { _mToken.setBalanceOf(_carol, 100_000000); 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); @@ -115,7 +118,7 @@ contract StoryTests is Test { _mToken.setBalanceOf(_bob, 100_000000); vm.prank(_bob); - _wrappedMToken.wrap(_bob, 100_000000); + _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _bob); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), 100_000000); @@ -132,7 +135,7 @@ contract StoryTests is Test { _mToken.setBalanceOf(_dave, 100_000000); 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); @@ -307,7 +310,10 @@ contract StoryTests is Test { assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_alice); - _wrappedMToken.unwrap(_alice, 266_666664); + _wrappedMToken.approve(address(_swapFacility), 266_666664); + + vm.prank(_alice); + _swapFacility.swapOutM(address(_wrappedMToken), 266_666664, _alice); // Assert Alice (Non-Earner) assertEq(_wrappedMToken.balanceOf(_alice), 0); @@ -321,7 +327,10 @@ contract StoryTests is Test { assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_bob); - _wrappedMToken.unwrap(_bob, 150_000000); + _wrappedMToken.approve(address(_swapFacility), 150_000000); + + vm.prank(_bob); + _swapFacility.swapOutM(address(_wrappedMToken), 150_000000, _bob); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), 0); @@ -335,7 +344,10 @@ contract StoryTests is Test { assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_carol); - _wrappedMToken.unwrap(_carol, 200_000000); + _wrappedMToken.approve(address(_swapFacility), 200_000000); + + vm.prank(_carol); + _swapFacility.swapOutM(address(_wrappedMToken), 200_000000, _carol); // Assert Carol (Earner) assertEq(_wrappedMToken.balanceOf(_carol), 0); @@ -349,7 +361,10 @@ contract StoryTests is Test { assertEq(_wrappedMToken.excess(), 599_999996); vm.prank(_dave); - _wrappedMToken.unwrap(_dave, 50_000000); + _wrappedMToken.approve(address(_swapFacility), 50_000000); + + vm.prank(_dave); + _swapFacility.swapOutM(address(_wrappedMToken), 50_000000, _dave); // Assert Dave (Non-Earner) assertEq(_wrappedMToken.balanceOf(_dave), 0); @@ -377,7 +392,7 @@ contract StoryTests is Test { for (uint256 i_; i_ < 100; ++i_) { vm.prank(_alice); - _wrappedMToken.wrap(_alice, 9); + _swapFacility.swapInM(address(_wrappedMToken), 9, _alice); assertLe( int256(_wrappedMToken.balanceOf(_alice)) + int256(_wrappedMToken.excess()), @@ -398,8 +413,12 @@ contract StoryTests is Test { ); uint256 bobBalance_ = _wrappedMToken.balanceOf(_bob); + + vm.prank(_bob); + _wrappedMToken.approve(address(_swapFacility), bobBalance_); + vm.prank(_bob); - _wrappedMToken.unwrap(_bob, bobBalance_); + _swapFacility.swapOutM(address(_wrappedMToken), bobBalance_, _bob); } function test_dustWrapping() external { @@ -416,7 +435,7 @@ contract StoryTests is Test { for (uint256 i_; i_ < 100; ++i_) { vm.prank(_alice); - _wrappedMToken.wrap(_alice, 1); + _swapFacility.swapInM(address(_wrappedMToken), 1, _alice); assertLe( int256(_wrappedMToken.balanceOf(_alice)) + int256(_wrappedMToken.excess()), diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 32e1351..6de11d2 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -15,7 +15,7 @@ import { Test } from "../../lib/forge-std/src/Test.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { MockEarnerManager, MockM, MockRegistrar } from "../utils/Mocks.sol"; +import { MockEarnerManager, MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; import { WrappedMTokenHarness } from "../utils/WrappedMTokenHarness.sol"; // TODO: All operations involving earners should include demonstration of accrued yield being added to their balance. @@ -43,6 +43,7 @@ contract WrappedMTokenTests is Test { MockEarnerManager internal _earnerManager; MockM internal _mToken; MockRegistrar internal _registrar; + MockSwapFacility internal _swapFacility; WrappedMTokenHarness internal _implementation; WrappedMTokenHarness internal _wrappedMToken; @@ -53,11 +54,14 @@ contract WrappedMTokenTests is Test { _earnerManager = new MockEarnerManager(); + _swapFacility = new MockSwapFacility(address(_mToken)); + _implementation = new WrappedMTokenHarness( address(_mToken), address(_registrar), address(_earnerManager), _excessDestination, + address(_swapFacility), _migrationAdmin ); @@ -78,7 +82,8 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.mToken(), address(_mToken)); assertEq(_wrappedMToken.registrar(), address(_registrar)); assertEq(_wrappedMToken.excessDestination(), _excessDestination); - assertEq(_wrappedMToken.name(), "M (Wrapped) by M^0"); + 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)); @@ -88,17 +93,17 @@ contract WrappedMTokenTests is Test { function test_constructor_zeroMToken() external { vm.expectRevert(IWrappedMToken.ZeroMToken.selector); - new WrappedMTokenHarness(address(0), address(0), address(0), address(0), address(0)); + new WrappedMTokenHarness(address(0), address(0), 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), address(0)); + new WrappedMTokenHarness(address(_mToken), address(0), address(0), address(0), address(0), address(0)); } function test_constructor_zeroEarnerManager() external { vm.expectRevert(IWrappedMToken.ZeroEarnerManager.selector); - new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0), address(0)); + new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0), address(0), address(0)); } function test_constructor_zeroExcessDestination() external { @@ -108,6 +113,19 @@ contract WrappedMTokenTests is Test { address(_registrar), address(_earnerManager), address(0), + address(0), + address(0) + ); + } + + function test_constructor_zeroSwapFacility() external { + vm.expectRevert(IWrappedMToken.ZeroSwapFacility.selector); + new WrappedMTokenHarness( + address(_mToken), + address(_registrar), + address(_earnerManager), + _excessDestination, + address(0), address(0) ); } @@ -119,6 +137,7 @@ contract WrappedMTokenTests is Test { address(_registrar), address(_earnerManager), _excessDestination, + address(_swapFacility), address(0) ); } @@ -132,7 +151,8 @@ contract WrappedMTokenTests is Test { function test_internalWrap_insufficientAmount() external { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); - _wrappedMToken.internalWrap(_alice, _alice, 0); + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, 0); } function test_internalWrap_invalidRecipient() external { @@ -140,7 +160,8 @@ contract WrappedMTokenTests is Test { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); - _wrappedMToken.internalWrap(_alice, address(0), 1_000); + vm.prank(_alice); + _wrappedMToken.internalWrap(address(0), 1_000); } function test_internalWrap_toNonEarner() external { @@ -161,7 +182,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 1_000); - _wrappedMToken.internalWrap(_alice, _alice, 1_000); + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, 1_000); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 2_000); @@ -194,7 +216,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 999); - _wrappedMToken.internalWrap(_alice, _alice, 999); + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, 999); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908); assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999); @@ -207,7 +230,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 1); - _wrappedMToken.internalWrap(_alice, _alice, 1); + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, 1); // No change due to principal round down on wrap. assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0); @@ -221,7 +245,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 2); - _wrappedMToken.internalWrap(_alice, _alice, 2); + vm.prank(_alice); + _wrappedMToken.internalWrap(_alice, 2); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0 + 1); assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999 + 1 + 2); @@ -233,11 +258,19 @@ contract WrappedMTokenTests is Test { } /* ============ wrap ============ */ + function test_wrap_notSwapFacility() external { + vm.expectRevert(IWrappedMToken.NotSwapFacility.selector); + + vm.prank(_alice); + _wrappedMToken.wrap(_alice, 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); + _swapFacility.swapInM(address(_wrappedMToken), uint256(type(uint240).max) + 1, _alice); } function testFuzz_wrap( @@ -278,7 +311,7 @@ contract WrappedMTokenTests is Test { } vm.startPrank(_alice); - _wrappedMToken.wrap(_alice, wrapAmount_); + _swapFacility.swapInM(address(_wrappedMToken), wrapAmount_, _alice); if (wrapAmount_ == 0) return; @@ -299,137 +332,23 @@ contract WrappedMTokenTests is Test { vm.expectRevert(UIntMath.InvalidUInt240.selector); vm.prank(_alice); - _wrappedMToken.wrap(_alice, uint256(type(uint240).max) + 1); - } - - /* ============ wrapWithPermit vrs ============ */ - function test_wrapWithPermit_vrs_invalidAmount() external { - vm.expectRevert(UIntMath.InvalidUInt240.selector); - - vm.prank(_alice); - _wrappedMToken.wrapWithPermit(_alice, uint256(type(uint240).max) + 1, 0, 0, bytes32(0), bytes32(0)); - } - - function testFuzz_wrapWithPermit_vrs( - 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, 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.startPrank(_alice); - _wrappedMToken.wrapWithPermit(_alice, wrapAmount_, 0, 0, bytes32(0), bytes32(0)); - - if (wrapAmount_ == 0) return; - - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); - - assertEq( - accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), - _wrappedMToken.balanceOf(_alice) - ); - } - - /* ============ wrapWithPermit signature ============ */ - function test_wrapWithPermit_signature_invalidAmount() external { - vm.expectRevert(UIntMath.InvalidUInt240.selector); - - vm.prank(_alice); - _wrappedMToken.wrapWithPermit(_alice, uint256(type(uint240).max) + 1, 0, hex""); - } - - function testFuzz_wrapWithPermit_signature( - 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, 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.startPrank(_alice); - _wrappedMToken.wrapWithPermit(_alice, wrapAmount_, 0, hex""); - - if (wrapAmount_ == 0) return; - - assertEq(_wrappedMToken.balanceOf(_alice), balance_ + wrapAmount_); - - assertEq( - accountEarning_ ? _wrappedMToken.totalEarningSupply() : _wrappedMToken.totalNonEarningSupply(), - _wrappedMToken.balanceOf(_alice) - ); + _swapFacility.swapInM(address(_wrappedMToken), uint256(type(uint240).max) + 1, _alice); } /* ============ _unwrap ============ */ function test_internalUnwrap_insufficientAmount() external { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); - _wrappedMToken.internalUnwrap(_alice, _alice, 0); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(0); } function test_internalUnwrap_insufficientBalance_fromNonEarner() external { _wrappedMToken.setAccountOf(_alice, 999); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); - _wrappedMToken.internalUnwrap(_alice, _alice, 1_000); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(1_000); } function test_internalUnwrap_insufficientBalance_fromEarner() external { @@ -439,7 +358,8 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 999, 909, false, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); - _wrappedMToken.internalUnwrap(_alice, _alice, 1_000); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(1_000); } function test_internalUnwrap_fromNonEarner() external { @@ -464,7 +384,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 1); - _wrappedMToken.internalUnwrap(_alice, _alice, 1); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(1); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 999); @@ -477,7 +398,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 499); - _wrappedMToken.internalUnwrap(_alice, _alice, 499); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(499); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 500); @@ -490,7 +412,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 500); - _wrappedMToken.internalUnwrap(_alice, _alice, 500); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 0); @@ -524,7 +447,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 1); - _wrappedMToken.internalUnwrap(_alice, _alice, 1); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(1); // Change due to principal round up on unwrap. assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1); @@ -538,7 +462,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 499); - _wrappedMToken.internalUnwrap(_alice, _alice, 499); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(499); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1 - 454); assertEq(_wrappedMToken.balanceOf(_alice), 1_000 - 1 - 499); @@ -551,7 +476,8 @@ contract WrappedMTokenTests is Test { vm.expectEmit(); emit IERC20.Transfer(_alice, address(0), 500); - _wrappedMToken.internalUnwrap(_alice, _alice, 500); + vm.prank(_alice); + _wrappedMToken.internalUnwrap(500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1 - 454 - 455); // 0 assertEq(_wrappedMToken.balanceOf(_alice), 1_000 - 1 - 499 - 500); // 0 @@ -563,11 +489,21 @@ contract WrappedMTokenTests is Test { } /* ============ unwrap ============ */ + function test_unwrap_notSwapFacility() external { + vm.expectRevert(IWrappedMToken.NotSwapFacility.selector); + + vm.prank(_alice); + _wrappedMToken.unwrap(_alice, 1_000); + } + 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); - _wrappedMToken.unwrap(_alice, uint256(type(uint240).max) + 1); + _swapFacility.swapOutM(address(_wrappedMToken), uint256(type(uint240).max) + 1, _alice); } function testFuzz_unwrap( @@ -600,6 +536,9 @@ contract WrappedMTokenTests is Test { 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_) { @@ -608,11 +547,11 @@ contract WrappedMTokenTests is Test { ); } else { vm.expectEmit(); - emit IERC20.Transfer(_alice, address(0), unwrapAmount_); + emit IERC20.Transfer(address(_swapFacility), address(0), unwrapAmount_); } vm.startPrank(_alice); - _wrappedMToken.unwrap(_alice, unwrapAmount_); + _swapFacility.swapOutM(address(_wrappedMToken), unwrapAmount_, _alice); if ((unwrapAmount_ == 0) || (unwrapAmount_ > balance_)) return; diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index 41bfd42..4e50749 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.26; +import { IERC20 } from "../../lib/common/src/interfaces/IERC20.sol"; + contract MockM { uint128 public currentIndex; @@ -64,6 +66,10 @@ contract MockM { function stopEarning() external { isEarning[msg.sender] = false; } + + function approve(address spender_, uint256 amount_) external returns (bool success_) { + return true; + } } contract MockRegistrar { @@ -99,3 +105,32 @@ contract MockEarnerManager { return (earnerDetails_.status, earnerDetails_.feeRate, earnerDetails_.admin); } } + +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); + + amount = IERC20(mToken).balanceOf(address(this)) - balanceBefore; + IERC20(mToken).transfer(recipient, amount); + } +} diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index 5e35449..edae0f6 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -10,15 +10,16 @@ contract WrappedMTokenHarness is WrappedMToken { address registrar_, address earnerManager_, address excessDestination_, + address swapFacility_, address migrationAdmin_ - ) WrappedMToken(mToken_, registrar_, earnerManager_, excessDestination_, migrationAdmin_) {} + ) WrappedMToken(mToken_, registrar_, earnerManager_, excessDestination_, swapFacility_, migrationAdmin_) {} - function internalWrap(address account_, address recipient_, uint240 amount_) external { - _wrap(account_, recipient_, amount_); + function internalWrap(address recipient_, uint240 amount_) external { + _wrap(recipient_, amount_); } - function internalUnwrap(address account_, address recipient_, uint240 amount_) external { - _unwrap(account_, recipient_, amount_); + function internalUnwrap(uint240 amount_) external { + _unwrap(amount_); } function setIsEarningOf(address account_, bool isEarning_) external { From af67fcee864f855b1eef667ac3dc7654087106c0 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 16 Dec 2025 03:48:13 -0600 Subject: [PATCH 16/27] fix(WrappedMToken): remove EarnerManager (#124) * fix(WrappedMToken): remove EarnerManager * chore: rename M^0 to M0 --- .gitignore | 3 + foundry.toml | 2 +- script/Deploy.s.sol | 16 +- script/DeployBase.sol | 105 +-- script/DeployUpgradeMainnet.s.sol | 22 +- src/EarnerManager.sol | 257 -------- src/ListOfEarnersToMigrate.sol | 2 +- src/WrappedMToken.sol | 115 +--- src/WrappedMTokenMigratorV1.sol | 2 +- src/interfaces/IEarnerManager.sol | 2 +- src/interfaces/IMTokenLike.sol | 2 +- src/interfaces/IRegistrarLike.sol | 2 +- src/interfaces/IWrappedMToken.sol | 8 +- test/integration/Deploy.t.sol | 52 +- .../{Migration.sol => Migration.t.sol} | 272 ++++---- test/integration/MorphoBlue.t.sol | 22 +- test/integration/Protocol.t.sol | 100 ++- test/integration/TestBase.sol | 36 +- test/integration/UniswapV3.t.sol | 50 +- test/integration/Upgrade.t.sol | 54 +- .../vendor/protocol/Interfaces.sol | 10 + test/unit/EarnerManager.sol | 598 ------------------ test/unit/Migrations.t.sol | 72 +-- test/unit/Stories.t.sol | 22 +- test/unit/WrappedMToken.t.sol | 219 ++----- test/utils/EarnerManagerHarness.sol | 17 - test/utils/Mocks.sol | 20 - test/utils/WrappedMTokenHarness.sol | 40 +- 28 files changed, 429 insertions(+), 1693 deletions(-) delete mode 100644 src/EarnerManager.sol rename test/integration/{Migration.sol => Migration.t.sol} (75%) delete mode 100644 test/unit/EarnerManager.sol delete mode 100644 test/utils/EarnerManagerHarness.sol 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/foundry.toml b/foundry.toml index b014462..69a589d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ solc_version = "0.8.26" optimizer = true optimizer_runs = 999999 verbosity = 3 -fork_block_number = 21_345_650 +fork_block_number = 23_170_985 rpc_storage_caching = { chains = ["mainnet"], endpoints = "all" } evm_version = "cancun" diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 9a6403f..7e18fb0 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -34,20 +34,13 @@ contract DeployProduction is Script, DeployBase { uint64 currentNonce_ = vm.getNonce(deployer_); uint64 startNonce_ = currentNonce_; - address earnerManagerImplementation_; - address earnerManagerProxy_; address wrappedMTokenImplementation_; address wrappedMTokenProxy_; while (true) { if (startNonce_ > deployerWrappedMProxyNonce_) revert DeployerNonceTooHigh(); - ( - earnerManagerImplementation_, - earnerManagerProxy_, - wrappedMTokenImplementation_, - wrappedMTokenProxy_ - ) = mockDeploy(deployer_, startNonce_); + (wrappedMTokenImplementation_, wrappedMTokenProxy_) = mockDeploy(deployer_, startNonce_); if (wrappedMTokenProxy_ == expectedWrappedMProxy_) break; @@ -66,21 +59,18 @@ contract DeployProduction is Script, DeployBase { if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); - (earnerManagerImplementation_, earnerManagerProxy_, wrappedMTokenImplementation_, wrappedMTokenProxy_) = deploy( + (wrappedMTokenImplementation_, wrappedMTokenProxy_) = deploy( vm.envAddress("M_TOKEN"), vm.envAddress("REGISTRAR"), vm.envAddress("EXCESS_DESTINATION"), vm.envAddress("SWAP_FACILITY"), - vm.envAddress("WRAPPED_M_MIGRATION_ADMIN"), - vm.envAddress("EARNER_MANAGER_MIGRATION_ADMIN") + vm.envAddress("WRAPPED_M_MIGRATION_ADMIN") ); vm.stopBroadcast(); console2.log("Wrapped M Implementation address:", wrappedMTokenImplementation_); console2.log("Wrapped M Proxy address:", wrappedMTokenProxy_); - console2.log("Earner Manager Implementation address:", earnerManagerImplementation_); - console2.log("Earner Manager Proxy address:", earnerManagerProxy_); if (wrappedMTokenProxy_ != expectedWrappedMProxy_) revert ResultingProxyMismatch(expectedWrappedMProxy_, wrappedMTokenProxy_); diff --git a/script/DeployBase.sol b/script/DeployBase.sol index 6313927..4e15a9e 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -5,7 +5,6 @@ pragma solidity 0.8.26; import { ContractHelper } from "../lib/common/src/libs/ContractHelper.sol"; import { Proxy } from "../lib/common/src/Proxy.sol"; -import { EarnerManager } from "../src/EarnerManager.sol"; import { WrappedMTokenMigratorV1 } from "../src/WrappedMTokenMigratorV1.sol"; import { WrappedMToken } from "../src/WrappedMToken.sol"; @@ -17,9 +16,6 @@ contract DeployBase { * @param swapFacility_ The address of the SwapFacility contract. * @param excessDestination_ The address of the excess destination. * @param wrappedMMigrationAdmin_ The address of the Wrapped M Migration Admin. - * @param earnerManagerMigrationAdmin_ The address of the Earner Manager Migration Admin. - * @return earnerManagerImplementation_ The address of the deployed Earner Manager implementation. - * @return earnerManagerProxy_ The address of the deployed Earner Manager proxy. * @return wrappedMTokenImplementation_ The address of the deployed Wrapped M Token implementation. * @return wrappedMTokenProxy_ The address of the deployed Wrapped M Token proxy. */ @@ -28,29 +24,10 @@ contract DeployBase { address registrar_, address excessDestination_, address swapFacility_, - address wrappedMMigrationAdmin_, - address earnerManagerMigrationAdmin_ - ) - public - virtual - returns ( - address earnerManagerImplementation_, - address earnerManagerProxy_, - address wrappedMTokenImplementation_, - address wrappedMTokenProxy_ - ) - { - // Earner Manager Implementation constructor needs only known values. - // Earner Manager Proxy constructor needs `earnerManagerImplementation_`. - // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. - // Wrapped M Token Proxy constructor needs `wrappedMTokenImplementation_`. - - earnerManagerImplementation_ = address(new EarnerManager(registrar_, earnerManagerMigrationAdmin_)); - - earnerManagerProxy_ = address(new Proxy(earnerManagerImplementation_)); - + address wrappedMMigrationAdmin_ + ) public virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenProxy_) { wrappedMTokenImplementation_ = address( - new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) + new WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) ); wrappedMTokenProxy_ = address(new Proxy(wrappedMTokenImplementation_)); @@ -63,9 +40,6 @@ contract DeployBase { * @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 earnerManagerMigrationAdmin_ The address of the Earner Manager Migration Admin. - * @return earnerManagerImplementation_ The address of the deployed Earner Manager implementation. - * @return earnerManagerProxy_ The address of the deployed Earner Manager proxy. * @return wrappedMTokenImplementation_ The address of the deployed Wrapped M Token implementation. * @return wrappedMTokenMigrator_ The address of the deployed Wrapped M Token Migrator. */ @@ -75,29 +49,10 @@ contract DeployBase { address excessDestination_, address swapFacility_, address wrappedMMigrationAdmin_, - address earnerManagerMigrationAdmin_, address[] memory earners_ - ) - public - virtual - returns ( - address earnerManagerImplementation_, - address earnerManagerProxy_, - address wrappedMTokenImplementation_, - address wrappedMTokenMigrator_ - ) - { - // Earner Manager Implementation constructor needs only known values. - // Earner Manager Proxy constructor needs `earnerManagerImplementation_`. - // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. - // Migrator needs `wrappedMTokenImplementation_` addresses. - - earnerManagerImplementation_ = address(new EarnerManager(registrar_, earnerManagerMigrationAdmin_)); - - earnerManagerProxy_ = address(new Proxy(earnerManagerImplementation_)); - + ) public virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) { wrappedMTokenImplementation_ = address( - new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) + new WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) ); wrappedMTokenMigrator_ = address(new WrappedMTokenMigratorV1(wrappedMTokenImplementation_, earners_)); @@ -107,67 +62,29 @@ contract DeployBase { * @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 earnerManagerImplementation_ The address of the would-be Earner Manager implementation. - * @return earnerManagerProxy_ The address of the would-be Earner Manager proxy. * @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 - view - virtual - returns ( - address earnerManagerImplementation_, - address earnerManagerProxy_, - address wrappedMTokenImplementation_, - address wrappedMTokenProxy_ - ) - { - // Earner Manager Implementation constructor needs only known values. - // Earner Manager Proxy constructor needs `earnerManagerImplementation_`. - // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. - // Wrapped M Token Proxy constructor needs `wrappedMTokenImplementation_`. - - earnerManagerImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); - earnerManagerProxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); - wrappedMTokenImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 2); - wrappedMTokenProxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 3); + ) public view virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenProxy_) { + wrappedMTokenImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + wrappedMTokenProxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); } /** * @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 earnerManagerImplementation_ The address of the would-be Earner Manager implementation. - * @return earnerManagerProxy_ The address of the would-be Earner Manager proxy. * @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 earnerManagerImplementation_, - address earnerManagerProxy_, - address wrappedMTokenImplementation_, - address wrappedMTokenMigrator_ - ) - { - // Earner Manager Implementation constructor needs only known values. - // Earner Manager Proxy constructor needs `earnerManagerImplementation_`. - // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. - // Migrator needs `wrappedMTokenImplementation_` addresses. - - earnerManagerImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); - earnerManagerProxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); - wrappedMTokenImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 2); - wrappedMTokenMigrator_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 3); + ) public view virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) { + wrappedMTokenImplementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + wrappedMTokenMigrator_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); } } diff --git a/script/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol index 271b8cc..548ab50 100644 --- a/script/DeployUpgradeMainnet.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -22,8 +22,7 @@ contract DeployUpgradeMainnet is Script, DeployBase { // NOTE: Ensure this is the correct Excess Destination mainnet address. address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault - // TODO: Replace with the correct Swap Facility mainnet address after deployment. - address internal constant _SWAP_FACILITY = address(0); + address internal constant _SWAP_FACILITY = 0xB6807116b3B1B321a390594e31ECD6e0076f6278; address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; @@ -93,20 +92,13 @@ contract DeployUpgradeMainnet is Script, DeployBase { uint64 currentNonce_ = vm.getNonce(deployer_); uint64 startNonce_ = currentNonce_; - address earnerManagerImplementation_; - address earnerManagerProxy_; address wrappedMTokenImplementation_; address wrappedMTokenMigrator_; while (true) { if (startNonce_ > _DEPLOYER_MIGRATOR_NONCE) revert DeployerNonceTooHigh(); - ( - earnerManagerImplementation_, - earnerManagerProxy_, - wrappedMTokenImplementation_, - wrappedMTokenMigrator_ - ) = mockDeployUpgrade(deployer_, startNonce_); + (wrappedMTokenImplementation_, wrappedMTokenMigrator_) = mockDeployUpgrade(deployer_, startNonce_); if (wrappedMTokenMigrator_ == _EXPECTED_WRAPPED_M_MIGRATOR) break; @@ -131,25 +123,17 @@ contract DeployUpgradeMainnet is Script, DeployBase { earners_[index_] = _earners[index_]; } - ( - earnerManagerImplementation_, - earnerManagerProxy_, - wrappedMTokenImplementation_, - wrappedMTokenMigrator_ - ) = deployUpgrade( + (wrappedMTokenImplementation_, wrappedMTokenMigrator_) = deployUpgrade( _M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, - _EARNER_MANAGER_MIGRATION_ADMIN, earners_ ); vm.stopBroadcast(); - console2.log("Earner Manager Implementation address:", earnerManagerImplementation_); - console2.log("Earner Manager Proxy address:", earnerManagerProxy_); console2.log("Wrapped M Implementation address:", wrappedMTokenImplementation_); console2.log("Migrator address:", wrappedMTokenMigrator_); diff --git a/src/EarnerManager.sol b/src/EarnerManager.sol deleted file mode 100644 index 04d0e91..0000000 --- a/src/EarnerManager.sol +++ /dev/null @@ -1,257 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.26; - -import { Migratable } from "../lib/common/src/Migratable.sol"; - -import { IEarnerManager } from "./interfaces/IEarnerManager.sol"; -import { IRegistrarLike } from "./interfaces/IRegistrarLike.sol"; - -/** - * @title Earner Manager allows admins to define earners without governance, and take fees from yield. - * @author M^0 Labs - */ -contract EarnerManager is IEarnerManager, Migratable { - /* ============ Structs ============ */ - - struct EarnerDetails { - address admin; - uint16 feeRate; - } - - /* ============ Variables ============ */ - - /// @inheritdoc IEarnerManager - uint16 public constant MAX_FEE_RATE = 10_000; - - /// @inheritdoc IEarnerManager - bytes32 public constant ADMINS_LIST_NAME = "em_admins"; - - /// @inheritdoc IEarnerManager - bytes32 public constant EARNERS_LIST_IGNORED_KEY = "earners_list_ignored"; - - /// @inheritdoc IEarnerManager - bytes32 public constant EARNERS_LIST_NAME = "earners"; - - /// @inheritdoc IEarnerManager - bytes32 public constant MIGRATOR_KEY_PREFIX = "em_migrator_v1"; - - /// @inheritdoc IEarnerManager - address public immutable registrar; - - /// @inheritdoc IEarnerManager - address public immutable migrationAdmin; - - /// @dev Mapping of account to earner details. - mapping(address account => EarnerDetails earnerDetails) internal _earnerDetails; - - /* ============ Modifiers ============ */ - - modifier onlyAdmin() { - _revertIfNotAdmin(); - _; - } - - /* ============ Constructor ============ */ - - /** - * @dev Constructs the contract. - * @param registrar_ The address of a Registrar contract. - * @param migrationAdmin_ The address of a migration admin. - */ - constructor(address registrar_, address migrationAdmin_) { - if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); - if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); - } - - /* ============ Interactive Functions ============ */ - - /// @inheritdoc IEarnerManager - function setEarnerDetails(address account_, bool status_, uint16 feeRate_) external onlyAdmin { - if (earnersListsIgnored()) revert EarnersListsIgnored(); - - _setDetails(account_, status_, feeRate_); - } - - /// @inheritdoc IEarnerManager - function setEarnerDetails( - address[] calldata accounts_, - bool[] calldata statuses_, - uint16[] calldata feeRates_ - ) external onlyAdmin { - if (accounts_.length == 0) revert ArrayLengthZero(); - if (accounts_.length != statuses_.length) revert ArrayLengthMismatch(); - if (accounts_.length != feeRates_.length) revert ArrayLengthMismatch(); - if (earnersListsIgnored()) revert EarnersListsIgnored(); - - for (uint256 index_; index_ < accounts_.length; ++index_) { - // NOTE: The `isAdmin` check in `_setDetails` will make this costly to re-set details for multiple accounts - // that have already been set by the same admin, due to the redundant queries to the registrar. - // Consider transient storage in `isAdmin` to memoize admins. - _setDetails(accounts_[index_], statuses_[index_], feeRates_[index_]); - } - } - - /* ============ Temporary Admin Migration ============ */ - - /// @inheritdoc IEarnerManager - function migrate(address migrator_) external { - if (msg.sender != migrationAdmin) revert UnauthorizedMigration(); - - _migrate(migrator_); - } - - /* ============ View/Pure Functions ============ */ - - /// @inheritdoc IEarnerManager - function earnerStatusFor(address account_) external view returns (bool status_) { - return earnersListsIgnored() || isInRegistrarEarnersList(account_) || isInAdministratedEarnersList(account_); - } - - /// @inheritdoc IEarnerManager - function earnerStatusesFor(address[] calldata accounts_) external view returns (bool[] memory statuses_) { - statuses_ = new bool[](accounts_.length); - - bool earnersListsIgnored_ = earnersListsIgnored(); - - for (uint256 index_; index_ < accounts_.length; ++index_) { - if (earnersListsIgnored_) { - statuses_[index_] = true; - continue; - } - - address account_ = accounts_[index_]; - - if (isInRegistrarEarnersList(account_)) { - statuses_[index_] = true; - continue; - } - - statuses_[index_] = isInAdministratedEarnersList(account_); - } - } - - /// @inheritdoc IEarnerManager - function earnersListsIgnored() public view returns (bool isIgnored_) { - return IRegistrarLike(registrar).get(EARNERS_LIST_IGNORED_KEY) != bytes32(0); - } - - /// @inheritdoc IEarnerManager - function isInRegistrarEarnersList(address account_) public view returns (bool isInList_) { - return IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, account_); - } - - /// @inheritdoc IEarnerManager - function isInAdministratedEarnersList(address account_) public view returns (bool isInList_) { - return _isValidAdmin(_earnerDetails[account_].admin); - } - - /// @inheritdoc IEarnerManager - function getEarnerDetails(address account_) external view returns (bool status_, uint16 feeRate_, address admin_) { - if (earnersListsIgnored() || isInRegistrarEarnersList(account_)) return (true, 0, address(0)); - - EarnerDetails memory details_ = _earnerDetails[account_]; - - // NOTE: Not using `isInAdministratedEarnersList(account_)` here to avoid redundant storage reads. - return _isValidAdmin(details_.admin) ? (true, details_.feeRate, details_.admin) : (false, 0, address(0)); - } - - /// @inheritdoc IEarnerManager - function getEarnerDetails( - address[] calldata accounts_ - ) external view returns (bool[] memory statuses_, uint16[] memory feeRates_, address[] memory admins_) { - statuses_ = new bool[](accounts_.length); - feeRates_ = new uint16[](accounts_.length); - admins_ = new address[](accounts_.length); - - bool earnersListsIgnored_ = earnersListsIgnored(); - - for (uint256 index_; index_ < accounts_.length; ++index_) { - if (earnersListsIgnored_) { - statuses_[index_] = true; - continue; - } - - address account_ = accounts_[index_]; - - if (isInRegistrarEarnersList(account_)) { - statuses_[index_] = true; - continue; - } - - EarnerDetails memory details_ = _earnerDetails[account_]; - - // NOTE: Not using `isInAdministratedEarnersList(account_)` here to avoid redundant storage reads. - if (!_isValidAdmin(details_.admin)) continue; - - statuses_[index_] = true; - feeRates_[index_] = details_.feeRate; - admins_[index_] = details_.admin; - } - } - - /// @inheritdoc IEarnerManager - function isAdmin(address account_) public view returns (bool isAdmin_) { - return IRegistrarLike(registrar).listContains(ADMINS_LIST_NAME, account_); - } - - /* ============ Internal Interactive Functions ============ */ - - /** - * @dev Sets the earner details for `account_`, assuming `msg.sender` is the calling admin. - * @param account_ The account under which yield could generate. - * @param status_ Whether the account is an earner, according to the admin. - * @param feeRate_ The fee rate to be taken from the yield. - */ - function _setDetails(address account_, bool status_, uint16 feeRate_) internal { - if (account_ == address(0)) revert ZeroAccount(); - if (!status_ && (feeRate_ != 0)) revert InvalidDetails(); // Fee rate must be zero if status is false. - if (feeRate_ > MAX_FEE_RATE) revert FeeRateTooHigh(); - if (isInRegistrarEarnersList(account_)) revert AlreadyInRegistrarEarnersList(account_); - - address admin_ = _earnerDetails[account_].admin; - - // Revert if the details have already been set by an admin that is not `msg.sender`, and is still an admin. - // NOTE: No `_isValidAdmin` here to avoid unnecessary contract call and storage reads if `admin_ == msg.sender`. - if ((admin_ != address(0)) && (admin_ != msg.sender) && isAdmin(admin_)) { - revert EarnerDetailsAlreadySet(account_); - } - - if (status_) { - _earnerDetails[account_] = EarnerDetails(msg.sender, feeRate_); - } else { - delete _earnerDetails[account_]; - } - - emit EarnerDetailsSet(account_, status_, msg.sender, feeRate_); - } - - /** - * @dev Reverts if the caller is not an admin. - */ - function _revertIfNotAdmin() internal view { - if (!isAdmin(msg.sender)) revert NotAdmin(); - } - - /* ============ Internal View/Pure Functions ============ */ - - /// @dev Returns the address of the contract to use as a migrator, if any. - function _getMigrator() internal view override returns (address migrator_) { - return - address( - uint160( - // NOTE: A subsequent implementation should use a unique migrator prefix. - uint256(IRegistrarLike(registrar).get(keccak256(abi.encode(MIGRATOR_KEY_PREFIX, address(this))))) - ) - ); - } - - /** - * @dev Returns whether `admin_` is a valid current admin. - * @param admin_ The admin to check. - * @return isValidAdmin_ True if `admin_` is a valid admin (non-zero and an admin according to the Registrar). - */ - function _isValidAdmin(address admin_) internal view returns (bool isValidAdmin_) { - return (admin_ != address(0)) && isAdmin(admin_); - } -} diff --git a/src/ListOfEarnersToMigrate.sol b/src/ListOfEarnersToMigrate.sol index 4c0b8ea..1fa76da 100644 --- a/src/ListOfEarnersToMigrate.sol +++ b/src/ListOfEarnersToMigrate.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.26; /** * @title Helper contract to retrieve earners for migrating a WrappedMToken contract from V1 to V2. - * @author M^0 Labs + * @author M0 Labs */ contract ListOfEarnersToMigrate { address[] public earners; diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 37fe39e..156dd81 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -10,7 +10,6 @@ import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol"; import { ERC20Extended } from "../lib/common/src/ERC20Extended.sol"; import { Migratable } from "../lib/common/src/Migratable.sol"; -import { IEarnerManager } from "./interfaces/IEarnerManager.sol"; import { IMTokenLike } from "./interfaces/IMTokenLike.sol"; import { IRegistrarLike } from "./interfaces/IRegistrarLike.sol"; import { IWrappedMToken } from "./interfaces/IWrappedMToken.sol"; @@ -39,7 +38,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @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. - * @param hasEarnerDetails Whether the account has additional details for earning yield. */ struct Account { // First Slot @@ -48,7 +46,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { // Second slot uint112 earningPrincipal; bool hasClaimRecipient; - bool hasEarnerDetails; } /* ============ Variables ============ */ @@ -68,9 +65,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken bytes32 public constant MIGRATOR_KEY_PREFIX = "wm_migrator_v2"; - /// @inheritdoc IWrappedMToken - address public immutable earnerManager; - /// @inheritdoc IWrappedMToken address public immutable migrationAdmin; @@ -121,7 +115,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * 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 earnerManager_ The address of an Earner Manager. * @param excessDestination_ The address of an excess destination. * @param swapFacility_ The address of a Swap Facility. * @param migrationAdmin_ The address of a migration admin. @@ -129,14 +122,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { constructor( address mToken_, address registrar_, - address earnerManager_, address excessDestination_, address swapFacility_, address migrationAdmin_ ) ERC20Extended("M (Wrapped) by M0", "wM", 6) { if ((mToken = mToken_) == address(0)) revert ZeroMToken(); if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); - if ((earnerManager = earnerManager_) == address(0)) revert ZeroEarnerManager(); if ((excessDestination = excessDestination_) == address(0)) revert ZeroExcessDestination(); if ((swapFacility = swapFacility_) == address(0)) revert ZeroSwapFacility(); if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); @@ -173,7 +164,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function enableEarning() external { - if (!_isThisApprovedEarner()) revert NotApprovedEarner(address(this)); + _revertIfNotApprovedEarner(address(this)); if (isEarningEnabled()) revert EarningIsEnabled(); emit EarningEnabled(enableMIndex = _currentMIndex()); @@ -183,7 +174,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function disableEarning() external { - if (_isThisApprovedEarner()) revert IsApprovedEarner(address(this)); + _revertIfApprovedEarner(address(this)); if (!isEarningEnabled()) revert EarningIsDisabled(); emit EarningDisabled(disableIndex = currentIndex()); @@ -260,9 +251,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function balanceWithYieldOf(address account_) external view returns (uint256 balance_) { - // NOTE: The returned amount includes the total accrued yield, regardless of whether it is split between the claim recipient and the earner manager. - // 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_` and may be split between the earner manager and the `account_`. + // 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_); } @@ -482,50 +472,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { emit Claimed(account_, claimRecipient_, yield_); emit Transfer(address(0), account_, yield_); - uint240 yieldNetOfFees_ = yield_; - - if (accountInfo_.hasEarnerDetails) { - unchecked { - yieldNetOfFees_ -= _handleEarnerDetails(account_, yield_, currentIndex_); - } - } - - if ((claimRecipient_ == account_) || (yieldNetOfFees_ == 0)) return yield_; - - _transfer(account_, claimRecipient_, yieldNetOfFees_, currentIndex_); - } - - /** - * @dev Handles the computation and transfer of fees to the admin of an account with earner details. - * @param account_ The address of the account to handle earner details for. - * @param yield_ The yield accrued by the account. - * @param currentIndex_ The current index to use to compute the principal amount. - * @return fee_ The fee amount that was transferred to the admin. - */ - function _handleEarnerDetails( - address account_, - uint240 yield_, - uint128 currentIndex_ - ) internal returns (uint240 fee_) { - (, uint16 feeRate_, address admin_) = _getEarnerDetails(account_); - - if (admin_ == address(0)) { - // Prevent transferring to address(0) and remove `hasEarnerDetails` property going forward. - _accounts[account_].hasEarnerDetails = false; - return 0; - } - - if (feeRate_ == 0) return 0; - - feeRate_ = feeRate_ > HUNDRED_PERCENT ? HUNDRED_PERCENT : feeRate_; // Ensure fee rate is capped at 100%. - - unchecked { - fee_ = (feeRate_ * yield_) / HUNDRED_PERCENT; - } - - if (fee_ == 0) return 0; + if ((claimRecipient_ == account_) || (yield_ == 0)) return yield_; - _transfer(account_, admin_, fee_, currentIndex_); + _transfer(account_, claimRecipient_, yield_, currentIndex_); } /** @@ -693,9 +642,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param currentIndex_ The current index. */ function _startEarningFor(address account_, uint128 currentIndex_) internal { - (bool isEarner_, , address admin_) = _getEarnerDetails(account_); - - if (!isEarner_) revert NotApprovedEarner(account_); + _revertIfNotApprovedEarner(account_); Account storage accountInfo_ = _accounts[account_]; @@ -710,9 +657,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { accountInfo_.isEarning = true; accountInfo_.earningPrincipal = principalDown_; - accountInfo_.hasEarnerDetails = admin_ != address(0); // Has earner details if an admin exists for this account. _addTotalEarningSupply(balance_, principalUp_); + unchecked { totalNonEarningSupply -= balance_; } @@ -726,9 +673,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param currentIndex_ The current index. */ function _stopEarningFor(address account_, uint128 currentIndex_) internal { - (bool isEarner_, , ) = _getEarnerDetails(account_); - - if (isEarner_) revert IsApprovedEarner(account_); + _revertIfApprovedEarner(account_); Account storage accountInfo_ = _accounts[account_]; @@ -741,7 +686,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { delete accountInfo_.isEarning; delete accountInfo_.earningPrincipal; - delete accountInfo_.hasEarnerDetails; _subtractTotalEarningSupply(balance_, earningPrincipal_); @@ -759,11 +703,15 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return IMTokenLike(mToken).currentIndex(); } - /// @dev Returns whether this contract is a Registrar-approved earner. - function _isThisApprovedEarner() internal view returns (bool) { + /** + * @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 - _getFromRegistrar(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || - IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, address(this)); + IRegistrarLike(registrar).get(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || + IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, account_); } /** @@ -785,19 +733,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } } - /** - * @dev Retrieves the earner details for `account`. - * @param account_ The account being queried. - * @return isEarner_ Whether the account is an earner. - * @return feeRate_ The fee rate to be taken from the yield. - * @return admin_ The admin who set the details and who will collect the fee. - */ - function _getEarnerDetails( - address account_ - ) internal view returns (bool isEarner_, uint16 feeRate_, address admin_) { - return IEarnerManager(earnerManager).getEarnerDetails(account_); - } - /** * @dev Retrieve a value from the Registrar. * @param key_ The key to retrieve the value for. @@ -843,4 +778,20 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { function _revertIfInvalidRecipient(address account_) internal pure { if (account_ == address(0)) revert InvalidRecipient(account_); } + + /** + * @dev Reverts if `account_` is an approved earner. + * @param account_ Address of an account. + */ + function _revertIfApprovedEarner(address account_) internal view { + if (_isApprovedEarner(account_)) revert IsApprovedEarner(account_); + } + + /** + * @dev Reverts if `account_` is not an approved earner. + * @param account_ Address of an account. + */ + function _revertIfNotApprovedEarner(address account_) internal view { + if (!_isApprovedEarner(account_)) revert NotApprovedEarner(account_); + } } diff --git a/src/WrappedMTokenMigratorV1.sol b/src/WrappedMTokenMigratorV1.sol index 9d6947b..5574543 100644 --- a/src/WrappedMTokenMigratorV1.sol +++ b/src/WrappedMTokenMigratorV1.sol @@ -8,7 +8,7 @@ import { ListOfEarnersToMigrate } from "./ListOfEarnersToMigrate.sol"; /** * @title Migrator contract for migrating a WrappedMToken contract from V1 to V2. - * @author M^0 Labs + * @author M0 Labs */ contract WrappedMTokenMigratorV1 { /// @notice Emitted when the `enableDisableEarningIndices` array has an invalid length. diff --git a/src/interfaces/IEarnerManager.sol b/src/interfaces/IEarnerManager.sol index 1a02fa2..a98eb1f 100644 --- a/src/interfaces/IEarnerManager.sol +++ b/src/interfaces/IEarnerManager.sol @@ -6,7 +6,7 @@ import { IMigratable } from "../../lib/common/src/interfaces/IMigratable.sol"; /** * @title Earner Status Manager interface for setting and returning earner status for Wrapped M Token accounts. - * @author M^0 Labs + * @author M0 Labs */ interface IEarnerManager is IMigratable { /* ============ Events ============ */ diff --git a/src/interfaces/IMTokenLike.sol b/src/interfaces/IMTokenLike.sol index 080e94d..2c22e59 100644 --- a/src/interfaces/IMTokenLike.sol +++ b/src/interfaces/IMTokenLike.sol @@ -4,7 +4,7 @@ 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 ============ */ diff --git a/src/interfaces/IRegistrarLike.sol b/src/interfaces/IRegistrarLike.sol index 2981059..8b6acbb 100644 --- a/src/interfaces/IRegistrarLike.sol +++ b/src/interfaces/IRegistrarLike.sol @@ -4,7 +4,7 @@ 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 ============ */ diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 496a008..0a2da10 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -7,7 +7,7 @@ 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 ============ */ @@ -88,9 +88,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @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 Earner Manager is 0x0. - error ZeroEarnerManager(); - /// @notice Emitted in constructor if Excess Destination is 0x0. error ZeroExcessDestination(); @@ -263,9 +260,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the Registrar. function registrar() external view returns (address registrar); - /// @notice The address of the Earner Manager. - function earnerManager() external view returns (address earnerManager); - /// @notice The portion of total supply that is not earning yield. function totalNonEarningSupply() external view returns (uint240 totalSupply); diff --git a/test/integration/Deploy.t.sol b/test/integration/Deploy.t.sol index 700ff84..dd92eaf 100644 --- a/test/integration/Deploy.t.sol +++ b/test/integration/Deploy.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.26; import { Test } from "../../lib/forge-std/src/Test.sol"; -import { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { DeployBase } from "../../script/DeployBase.sol"; @@ -13,52 +12,36 @@ contract DeployTests is Test, DeployBase { address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; - address internal constant _EARNER_MANAGER_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault - // TODO: Replace with the actual Swap Facility address after deployment. - address internal constant _SWAP_FACILITY = 0x0000000000000000000000000000000000000001; + address internal constant _SWAP_FACILITY = 0xB6807116b3B1B321a390594e31ECD6e0076f6278; address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; - uint64 internal constant _DEPLOYER_NONCE = 50; + uint64 internal constant _DEPLOYER_NONCE = 195; + + function setUp() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 23_170_985); + } function test_deploy() external { vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); - ( - address expectedEarnerManagerImplementation_, - address expectedEarnerManagerProxy_, - address expectedWrappedMTokenImplementation_, - address expectedWrappedMTokenProxy_ - ) = mockDeploy(_DEPLOYER, _DEPLOYER_NONCE); + (address expectedWrappedMTokenImplementation_, address expectedWrappedMTokenProxy_) = mockDeploy( + _DEPLOYER, + _DEPLOYER_NONCE + ); vm.startPrank(_DEPLOYER); - ( - address earnerManagerImplementation_, - address earnerManagerProxy_, - address wrappedMTokenImplementation_, - address wrappedMTokenProxy_ - ) = deploy( - _M_TOKEN, - _REGISTRAR, - _EXCESS_DESTINATION, - _SWAP_FACILITY, - _WRAPPED_M_MIGRATION_ADMIN, - _EARNER_MANAGER_MIGRATION_ADMIN - ); + (address wrappedMTokenImplementation_, address wrappedMTokenProxy_) = deploy( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _SWAP_FACILITY, + _WRAPPED_M_MIGRATION_ADMIN + ); vm.stopPrank(); - // Earner Manager Implementation assertions - assertEq(earnerManagerImplementation_, expectedEarnerManagerImplementation_); - assertEq(IEarnerManager(earnerManagerImplementation_).registrar(), _REGISTRAR); - - // Earner Manager Proxy assertions - assertEq(earnerManagerProxy_, expectedEarnerManagerProxy_); - assertEq(IEarnerManager(earnerManagerProxy_).registrar(), _REGISTRAR); - assertEq(IEarnerManager(earnerManagerProxy_).implementation(), earnerManagerImplementation_); - // Wrapped M Token Implementation assertions assertEq(wrappedMTokenImplementation_, expectedWrappedMTokenImplementation_); - assertEq(IWrappedMToken(wrappedMTokenImplementation_).earnerManager(), earnerManagerProxy_); assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); @@ -67,7 +50,6 @@ contract DeployTests is Test, DeployBase { // Wrapped M Token Proxy assertions assertEq(wrappedMTokenProxy_, expectedWrappedMTokenProxy_); - assertEq(IWrappedMToken(wrappedMTokenProxy_).earnerManager(), earnerManagerProxy_); assertEq(IWrappedMToken(wrappedMTokenProxy_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(wrappedMTokenProxy_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenProxy_).registrar(), _REGISTRAR); diff --git a/test/integration/Migration.sol b/test/integration/Migration.t.sol similarity index 75% rename from test/integration/Migration.sol rename to test/integration/Migration.t.sol index a14b025..f18928b 100644 --- a/test/integration/Migration.sol +++ b/test/integration/Migration.t.sol @@ -21,34 +21,40 @@ contract MigrationIntegrationTests is TestBase { 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90 ]; + function setUp() public override { + super.setUp(); + + vm.selectFork(mainnetFork); + } + function test_initialState() external view { - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); - - assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + 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]), 18_745_425_119664); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 184_519_881653); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 290_781_743197); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_542352); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 955_599930); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 69_560_490365); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 364_097752); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_771_521603); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 62_962_533531); - - assertEq(_wrappedMToken.currentIndex(), 1_073787769981); + 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 { @@ -58,31 +64,31 @@ contract MigrationIntegrationTests is TestBase { assertEq(_wrappedMToken.disableIndex(), 0); assertEq(_wrappedMToken.enableMIndex(), IndexingMath.EXP_SCALED_ONE); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + 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_023463403719); + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); vm.warp(vm.getBlockTimestamp() + 365 days); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 18_745_425_119664); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 184_519_881653); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 290_781_743197); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_542352); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 955_599930); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 69_560_490365); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 364_097752); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_771_521603); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 62_962_533531); - - assertEq(_wrappedMToken.currentIndex(), 1_073787769981); + 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 { @@ -93,53 +99,53 @@ contract MigrationIntegrationTests is TestBase { _deployV2Components(); _migrate(); - assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); assertEq(_wrappedMToken.enableMIndex(), 0); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + 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_023463403719); + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); _addToList(_EARNERS_LIST_NAME, address(_wrappedMToken)); _wrappedMToken.enableEarning(); - assertEq(_wrappedMToken.disableIndex(), 1_023463403719); - assertEq(_wrappedMToken.enableMIndex(), 1_023463403719); + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); + assertEq(_wrappedMToken.enableMIndex(), 1_054471748112); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + 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_023463403719); + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); vm.warp(vm.getBlockTimestamp() + 365 days); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 18_745_425_119664 - 35); // Rounding error - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 184_519_881653); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 290_781_743197); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_542352); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 955_599930); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 69_560_490365); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 364_097752); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_771_521603); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 62_962_533531); - - assertEq(_wrappedMToken.currentIndex(), 1_073787769981 - 2); // Rounding error + 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 { @@ -150,34 +156,34 @@ contract MigrationIntegrationTests is TestBase { _deployV2Components(); _migrate(); - assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); assertEq(_wrappedMToken.enableMIndex(), 0); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + 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_023463403719); + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); vm.warp(vm.getBlockTimestamp() + 365 days); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); - - assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + 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 { @@ -188,20 +194,20 @@ contract MigrationIntegrationTests is TestBase { _deployV2Components(); _migrate(); - assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); assertEq(_wrappedMToken.enableMIndex(), 0); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + 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_023463403719); + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); vm.warp(vm.getBlockTimestamp() + 365 days); @@ -209,33 +215,33 @@ contract MigrationIntegrationTests is TestBase { _wrappedMToken.enableEarning(); - assertEq(_wrappedMToken.disableIndex(), 1_023463403719); - assertEq(_wrappedMToken.enableMIndex(), 1_073787769979); + assertEq(_wrappedMToken.disableIndex(), 1_054471748112); + assertEq(_wrappedMToken.enableMIndex(), 1_099153050162); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 17_866_898_034674); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 175_872_133591); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 277_153_904106); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_470068); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 910_814580); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 66_300_453613); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 347_033869); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_641_630881); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 60_011_718029); + 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_023463403719); + assertEq(_wrappedMToken.currentIndex(), 1_054471748112); vm.warp(vm.getBlockTimestamp() + 365 days); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[0]), 18_745_425_119664 - 35); // Rounding error - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[1]), 184_519_881653); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[2]), 290_781_743197); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[3]), 1_542352); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[4]), 955_599930); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[5]), 69_560_490365); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[6]), 364_097752); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[7]), 2_771_521603); - assertEq(_wrappedMToken.balanceWithYieldOf(_holders[8]), 62_962_533531); - - assertEq(_wrappedMToken.currentIndex(), 1_073787769981 - 2); // Rounding error + 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 029ab2e..efdcb4e 100644 --- a/test/integration/MorphoBlue.t.sol +++ b/test/integration/MorphoBlue.t.sol @@ -25,7 +25,11 @@ contract MorphoBlueTests is MorphoTestBase { uint240 internal _excess; - function setUp() external { + function setUp() public override { + super.setUp(); + + vm.selectFork(mainnetFork); + _deployV2Components(); _migrate(); @@ -58,7 +62,7 @@ contract MorphoBlueTests is MorphoTestBase { _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 -= 1); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield); assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 1e6); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 1e6); @@ -95,7 +99,7 @@ contract MorphoBlueTests is MorphoTestBase { vm.warp(vm.getBlockTimestamp() + 365 days); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 49_292110); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 42_718348); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); @@ -110,7 +114,7 @@ contract MorphoBlueTests is MorphoTestBase { _withdrawCollateral(_bob, address(_wrappedMToken), 1_000e6, _bob, _USDC); // The collateral withdrawal has triggered a wM transfer but the yield has not been claimed for morpho. - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= 1); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield); assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM += 1_000e6); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM -= 1_000e6); @@ -128,7 +132,7 @@ contract MorphoBlueTests is MorphoTestBase { vm.warp(vm.getBlockTimestamp() + 365 days); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 2_545180); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 2_155299); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); @@ -150,7 +154,7 @@ contract MorphoBlueTests is MorphoTestBase { _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 -= 2); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield); assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 100000); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 100000); @@ -188,7 +192,7 @@ contract MorphoBlueTests is MorphoTestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 4_994266); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 4_544367); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); @@ -198,7 +202,7 @@ contract MorphoBlueTests is MorphoTestBase { _repay(_bob, address(_wrappedMToken), 900e6, _USDC); // The repay has triggered a wM transfer but the yield has not been claimed for morpho. - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield -= 1); // Rounding error assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 900e6); assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM += 900e6); @@ -222,7 +226,7 @@ contract MorphoBlueTests is MorphoTestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_MORPHO), _morphoBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 322772); + assertEq(_wrappedMToken.accruedYieldOf(_MORPHO), _morphoAccruedYield += 499611); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_MORPHO), _morphoBalanceOfUSDC); diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index d72f2d0..467f295 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -6,11 +6,14 @@ 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; @@ -29,7 +32,11 @@ contract ProtocolIntegrationTests is TestBase { int256 internal _excess; - function setUp() external { + function setUp() public override { + super.setUp(); + + vm.selectFork(mainnetFork); + _deployV2Components(); _migrate(); @@ -39,7 +46,20 @@ contract ProtocolIntegrationTests is TestBase { _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)); @@ -73,7 +93,9 @@ contract ProtocolIntegrationTests is TestBase { // Assert M Token assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 99_999999); - assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 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 = 100_000000); @@ -92,12 +114,16 @@ contract ProtocolIntegrationTests is TestBase { _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); @@ -126,7 +152,7 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 1_190592); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 1_028540); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -149,7 +175,9 @@ 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 = 200_000000); @@ -169,6 +197,8 @@ contract ProtocolIntegrationTests is TestBase { _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); @@ -199,12 +229,12 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += _aliceAccruedYield); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 1_190592); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 1_028540); // Assert Globals - assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 1_190592); + assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 1_028540); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1_190592); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1_028540); assertEq(_wrappedMToken.excess(), _excess); assertGe( @@ -224,11 +254,11 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_423881); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_088928); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield = 4_790723); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield = 4_135321); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -261,7 +291,7 @@ contract ProtocolIntegrationTests is TestBase { _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); @@ -271,7 +301,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); assertGe( int256(_wrapperBalanceOfM), @@ -290,7 +320,7 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield = 2_395361); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield = 2_067660); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -299,7 +329,7 @@ contract ProtocolIntegrationTests is TestBase { _giveM(_bob, 100_000000); _wrap(_bob, _bob, 100_000000); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 100_000000); + assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += (100_000000 - 1)); // Rounding error // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance = 100_000000); @@ -320,8 +350,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _bobBalance - _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _daveBalance + _aliceBalance); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 2); + assertEq(_wrappedMToken.excess(), _excess -= 3); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += _aliceBalance); @@ -329,7 +359,7 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield); assertGe( int256(_wrapperBalanceOfM), @@ -349,8 +379,8 @@ contract ProtocolIntegrationTests is TestBase { // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 50_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 50_000000); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -369,11 +399,11 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Earner) assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 57378); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 42752); // Assert Bob (Earner) assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 3_593042); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 3_101490); // Assert Carol (Non-Earner) assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance); @@ -403,13 +433,13 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 100_000000); assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += 100_000000); - assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 199_999999); + 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 -= 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); assertGe( int256(_wrapperBalanceOfM), @@ -430,18 +460,18 @@ contract ProtocolIntegrationTests is TestBase { _giveM(_dave, 100_000000); _wrap(_dave, _dave, 100_000000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 2_395361); + 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); + 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 -= 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); assertGe( int256(_wrapperBalanceOfM), @@ -456,8 +486,8 @@ contract ProtocolIntegrationTests is TestBase { _totalAccruedYield = _wrappedMToken.totalAccruedYield(); _excess = _wrappedMToken.excess(); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 1_219112); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_190593); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 1_049808); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_028540); // Stop earning for Alice _removeFromList(_EARNERS_LIST_NAME, _alice); @@ -466,13 +496,13 @@ contract ProtocolIntegrationTests is TestBase { // Assert Alice (Non-Earner) // Yield of Alice is claimed when stopping earning - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 3_614473); - assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 3_614473); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 3_117468); + assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 3_117468); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _aliceBalance); - assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614473 + 1); + assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_117468 + 1); assertEq(_wrappedMToken.excess(), _excess += 1); assertGe( @@ -508,8 +538,8 @@ contract ProtocolIntegrationTests is TestBase { _totalAccruedYield = _wrappedMToken.totalAccruedYield(); _excess = _wrappedMToken.excess(); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 2_423881); - assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield += 2_395361); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 2_088928); + assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield += 2_067660); assertGe( int256(_wrapperBalanceOfM), @@ -560,7 +590,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _carolBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield += 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess -= 2); // Assert Carol (Earner) assertEq(_mToken.balanceOf(_carol), _carolBalance); @@ -578,7 +608,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); // Assert Dave (Non-Earner) assertEq(_mToken.balanceOf(_dave), _daveBalance); @@ -600,7 +630,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= _excess); + assertEq(_wrappedMToken.excess(), _excess -= (_excess + 1)); // Rounding error assertGe( int256(_wrapperBalanceOfM), diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index b97a76d..e35cfc1 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -10,25 +10,25 @@ import { Test } from "../../lib/forge-std/src/Test.sol"; import { Proxy } from "../../lib/common/src/Proxy.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { ISwapFacilityLike } from "../../src/interfaces/ISwapFacilityLike.sol"; -import { EarnerManager } from "../../src/EarnerManager.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; import { WrappedMTokenMigratorV1 } from "../../src/WrappedMTokenMigratorV1.sol"; -import { IMTokenLike, IRegistrarLike } from "./vendor/protocol/Interfaces.sol"; +import { IMTokenLike, IRegistrarLike, ISwapFacilityLike } from "./vendor/protocol/Interfaces.sol"; import { MockSwapFacility } from "../utils/Mocks.sol"; contract TestBase is Test { + uint256 public mainnetFork; + IMTokenLike internal constant _mToken = IMTokenLike(0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b); address internal constant _minterGateway = 0xf7f9638cb444D65e5A40bF5ff98ebE4ff319F04E; address internal constant _registrar = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; address internal constant _excessDestination = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // vault address internal constant _standardGovernor = 0xB024aC5a7c6bC92fbACc8C3387E628a07e1Da016; - address internal constant _mSource = 0x563AA56D0B627d1A734e04dF5762F5Eea1D56C2f; - address internal constant _wmSource = 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D; + address internal constant _mSource = 0x3f0376da3Ae4313E7a5F1dA184BAFC716252d759; + address internal constant _wmSource = 0xfF95c5f35F4ffB9d5f596F898ac1ae38D62749c2; IWrappedMToken internal constant _wrappedMToken = IWrappedMToken(0x437cc33344a0B27A429f795ff6B469C72698B291); @@ -36,12 +36,13 @@ contract TestBase is Test { 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 = 0x4B16c5dE96EB2117bBE5fd171E4d203624B014aa; + address internal constant _USDC_SOURCE = 0x01b8697695EAb322A339c4bf75740Db75dc9375E; // DAI on Ethereum Mainnet address internal constant _DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; @@ -50,6 +51,7 @@ contract TestBase is Test { address internal constant _DAI_SOURCE = 0xD1668fB5F690C59Ab4B0CAbAd0f8C1617895052B; address internal _migrationAdmin = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + address internal _m0Deployer = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; address internal _alice = makeAddr("alice"); address internal _bob = makeAddr("bob"); @@ -66,13 +68,10 @@ contract TestBase is Test { address[] internal _accounts = [_alice, _bob, _carol, _dave, _eric, _frank, _grace, _henry, _ivan, _judy]; - address internal _earnerManagerImplementation; - address internal _earnerManager; address internal _wrappedMTokenImplementationV2; address internal _wrappedMTokenMigratorV1; - // TODO: Replace with the actual Swap Facility address after deployment. - address internal _swapFacility; + address internal _swapFacility = 0xB6807116b3B1B321a390594e31ECD6e0076f6278; address[] internal _earners = [ 0x437cc33344a0B27A429f795ff6B469C72698B291, @@ -112,6 +111,10 @@ contract TestBase is Test { 0x20b3a4119eAB75ffA534aC8fC5e9160BdcaF442b ]; + function setUp() public virtual { + 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; @@ -185,19 +188,8 @@ contract TestBase is Test { } function _deployV2Components() internal { - _earnerManagerImplementation = address(new EarnerManager(_registrar, _migrationAdmin)); - _earnerManager = address(new Proxy(_earnerManagerImplementation)); - // TODO: Replace with the actual Swap Facility address after deployment. - _swapFacility = address(new MockSwapFacility(address(_mToken))); _wrappedMTokenImplementationV2 = address( - new WrappedMToken( - address(_mToken), - _registrar, - _earnerManager, - _excessDestination, - _swapFacility, - _migrationAdmin - ) + new WrappedMToken(address(_mToken), _registrar, _excessDestination, _swapFacility, _migrationAdmin) ); address[] memory earners_ = new address[](_earners.length); diff --git a/test/integration/UniswapV3.t.sol b/test/integration/UniswapV3.t.sol index 6cfd256..25947ac 100644 --- a/test/integration/UniswapV3.t.sol +++ b/test/integration/UniswapV3.t.sol @@ -52,7 +52,11 @@ contract UniswapV3IntegrationTests is TestBase { int256 internal _excess; - function setUp() external { + function setUp() public override { + super.setUp(); + + vm.selectFork(mainnetFork); + _deployV2Components(); _migrate(); @@ -85,13 +89,13 @@ contract UniswapV3IntegrationTests is TestBase { _mintNewPosition(_alice, _alice, 1_000e6); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 999_930937); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 998_553467); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 998_553467); // The mint 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); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); @@ -103,7 +107,7 @@ contract UniswapV3IntegrationTests is TestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 878_576_252249); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 102_522_707768); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); @@ -134,7 +138,7 @@ contract UniswapV3IntegrationTests is TestBase { // `startEarningFor` has been called so wM yield has accrued in the pool. assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 921_727_256737); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 106_824_553128); // USDC balance is unchanged. assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC); @@ -158,7 +162,7 @@ contract UniswapV3IntegrationTests is TestBase { assertEq(_wrappedMToken.balanceOf(_poolClaimRecipient), _poolClaimRecipientBalanceOfWM); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); } function testFuzz_uniswapV3_earning(uint256 aliceAmount_, uint256 bobUsdc_, uint256 daveWrappedM_) public { @@ -277,13 +281,13 @@ contract UniswapV3IntegrationTests is TestBase { _mintNewPosition(_alice, _alice, 1_000e6); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 999_930937); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 998_553467); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 998_553467); // The mint 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); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield); assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); @@ -293,7 +297,7 @@ contract UniswapV3IntegrationTests is TestBase { // Move 10 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 10 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_512_966853); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 2_752_525465); /* ============ 2 Non-Earners and 2 Earners are Initialized ============ */ @@ -324,7 +328,7 @@ contract UniswapV3IntegrationTests is TestBase { // Move 1 day forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 1 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 2_353_129323); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 275_538415); // Claim yield for the pool and check that carol received yield. _wrappedMToken.claimFor(_pool); @@ -338,7 +342,7 @@ 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 += 11_753_024235); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1_376_440059); /* ============ Eric (Earner) Swaps Exact wM for USDC ============ */ @@ -356,7 +360,7 @@ contract UniswapV3IntegrationTests is TestBase { // Move 3 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 3 days); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 7_055_919498); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 826_580864); /* ============ Dave (Non-Earner) Swaps wM for Exact USDC ============ */ @@ -368,14 +372,14 @@ contract UniswapV3IntegrationTests is TestBase { // 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); + 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 += 16_475_562741); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 1_930_581718); /* ============ Frank (Earner) Swaps wM for Exact USDC ============ */ @@ -415,16 +419,16 @@ contract UniswapV3IntegrationTests is TestBase { (uint256 aliceTokenId_, , , ) = _mintNewPosition(_alice, _alice, 1_000e6); - assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 999_930937); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); + assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalanceOfWM -= 998_553467); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 998_553467); assertEq(IERC20(_USDC).balanceOf(_alice), _aliceBalanceOfUSDC -= 1_000e6); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); (uint256 bobTokenId_, , , ) = _mintNewPosition(_bob, _bob, 1_000e6); - assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 999_930937); - assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 999_930937); + assertEq(_wrappedMToken.balanceOf(_bob), _bobBalanceOfWM -= 998_553467); + assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 998_553467); assertEq(IERC20(_USDC).balanceOf(_bob), _bobBalanceOfUSDC -= 1_000e6); assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC += 1_000e6); @@ -436,8 +440,8 @@ contract UniswapV3IntegrationTests is TestBase { // Move 10 days forward and check that yield has accrued. vm.warp(vm.getBlockTimestamp() + 10 days); - assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_317340); - assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 23_514_282695); + assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield += 1_140416); + assertEq(_wrappedMToken.accruedYieldOf(_pool), _poolAccruedYield += 2_753_661453); /* ============ Dave (Non-Earner) Swaps Exact wM for USDC ============ */ @@ -447,7 +451,7 @@ contract UniswapV3IntegrationTests is TestBase { _swapExactInput(_dave, _dave, address(_wrappedMToken), _USDC, 1_000e6); - assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= 999_903365); + assertEq(IERC20(_USDC).balanceOf(_pool), _poolBalanceOfUSDC -= 999_970585); assertEq(_wrappedMToken.balanceOf(_pool), _poolBalanceOfWM += 1_000e6); diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 7b44dfd..86a054c 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.26; import { Test } from "../../lib/forge-std/src/Test.sol"; -import { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { DeployBase } from "../../script/DeployBase.sol"; @@ -14,13 +13,11 @@ contract UpgradeTests is Test, DeployBase { address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; - address internal constant _EARNER_MANAGER_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault - // TODO: Replace with the actual Swap Facility address after deployment. - address internal constant _SWAP_FACILITY = 0x0000000000000000000000000000000000000001; + address internal constant _SWAP_FACILITY = 0xB6807116b3B1B321a390594e31ECD6e0076f6278; address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; - uint64 internal constant _DEPLOYER_NONCE = 50; + uint64 internal constant _DEPLOYER_NONCE = 195; address[] internal _earners = [ 0x437cc33344a0B27A429f795ff6B469C72698B291, @@ -60,15 +57,17 @@ contract UpgradeTests is Test, DeployBase { 0x20b3a4119eAB75ffA534aC8fC5e9160BdcaF442b ]; + function setUp() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 23_170_985); + } + function test_upgrade() external { vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); - ( - address expectedEarnerManagerImplementation_, - address expectedEarnerManagerProxy_, - address expectedWrappedMTokenImplementation_, - address expectedWrappedMTokenMigrator_ - ) = mockDeployUpgrade(_DEPLOYER, _DEPLOYER_NONCE); + (address expectedWrappedMTokenImplementation_, address expectedWrappedMTokenMigrator_) = mockDeployUpgrade( + _DEPLOYER, + _DEPLOYER_NONCE + ); address[] memory earners_ = new address[](_earners.length); @@ -77,34 +76,18 @@ contract UpgradeTests is Test, DeployBase { } vm.startPrank(_DEPLOYER); - ( - address earnerManagerImplementation_, - address earnerManagerProxy_, - address wrappedMTokenImplementation_, - address wrappedMTokenMigrator_ - ) = deployUpgrade( - _M_TOKEN, - _REGISTRAR, - _EXCESS_DESTINATION, - _SWAP_FACILITY, - _WRAPPED_M_MIGRATION_ADMIN, - _EARNER_MANAGER_MIGRATION_ADMIN, - earners_ - ); + (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) = deployUpgrade( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _SWAP_FACILITY, + _WRAPPED_M_MIGRATION_ADMIN, + earners_ + ); vm.stopPrank(); - // Earner Manager Implementation assertions - assertEq(earnerManagerImplementation_, expectedEarnerManagerImplementation_); - assertEq(IEarnerManager(earnerManagerImplementation_).registrar(), _REGISTRAR); - - // Earner Manager Proxy assertions - assertEq(earnerManagerProxy_, expectedEarnerManagerProxy_); - assertEq(IEarnerManager(earnerManagerProxy_).registrar(), _REGISTRAR); - assertEq(IEarnerManager(earnerManagerProxy_).implementation(), earnerManagerImplementation_); - // Wrapped M Token Implementation assertions assertEq(wrappedMTokenImplementation_, expectedWrappedMTokenImplementation_); - assertEq(IWrappedMToken(wrappedMTokenImplementation_).earnerManager(), earnerManagerProxy_); assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); @@ -125,7 +108,6 @@ contract UpgradeTests is Test, DeployBase { IWrappedMToken(_WRAPPED_M_TOKEN).migrate(wrappedMTokenMigrator_); // Wrapped M Token Proxy assertions - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).earnerManager(), earnerManagerProxy_); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).mToken(), _M_TOKEN); assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).registrar(), _REGISTRAR); diff --git a/test/integration/vendor/protocol/Interfaces.sol b/test/integration/vendor/protocol/Interfaces.sol index dc549ec..f70dd17 100644 --- a/test/integration/vendor/protocol/Interfaces.sol +++ b/test/integration/vendor/protocol/Interfaces.sol @@ -27,3 +27,13 @@ interface IMTokenLike { 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/unit/EarnerManager.sol b/test/unit/EarnerManager.sol deleted file mode 100644 index 39c3e7d..0000000 --- a/test/unit/EarnerManager.sol +++ /dev/null @@ -1,598 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.26; - -import { Test } from "../../lib/forge-std/src/Test.sol"; - -import { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol"; - -import { MockRegistrar } from "./../utils/Mocks.sol"; -import { EarnerManagerHarness } from "../utils/EarnerManagerHarness.sol"; - -contract EarnerManagerTests is Test { - bytes32 internal constant _EARNERS_LIST_IGNORED_KEY = "earners_list_ignored"; - bytes32 internal constant _EARNERS_LIST_NAME = "earners"; - bytes32 internal constant _ADMINS_LIST_NAME = "em_admins"; - - address internal _admin1 = makeAddr("admin1"); - address internal _admin2 = makeAddr("admin2"); - - address internal _alice = makeAddr("alice"); - address internal _bob = makeAddr("bob"); - address internal _carol = makeAddr("carol"); - address internal _dave = makeAddr("dave"); - address internal _frank = makeAddr("frank"); - - address internal _migrationAdmin = makeAddr("migrationAdmin"); - - MockRegistrar internal _registrar; - EarnerManagerHarness internal _earnerManager; - - function setUp() external { - _registrar = new MockRegistrar(); - _earnerManager = new EarnerManagerHarness(address(_registrar), _migrationAdmin); - - _registrar.setListContains(_ADMINS_LIST_NAME, _admin1, true); - _registrar.setListContains(_ADMINS_LIST_NAME, _admin2, true); - } - - /* ============ initial state ============ */ - function test_initialState() external view { - assertEq(_earnerManager.registrar(), address(_registrar)); - } - - /* ============ constructor ============ */ - function test_constructor_zeroRegistrar() external { - vm.expectRevert(IEarnerManager.ZeroRegistrar.selector); - new EarnerManagerHarness(address(0), address(0)); - } - - function test_constructor_zeroMigrationAdmin() external { - vm.expectRevert(IEarnerManager.ZeroMigrationAdmin.selector); - new EarnerManagerHarness(address(_registrar), address(0)); - } - - /* ============ _setDetails ============ */ - function test_setDetails_zeroAccount() external { - vm.expectRevert(IEarnerManager.ZeroAccount.selector); - - _earnerManager.setDetails(address(0), false, 0); - } - - function test_setDetails_invalidDetails() external { - vm.expectRevert(IEarnerManager.InvalidDetails.selector); - - _earnerManager.setDetails(_alice, false, 1); - } - - function test_setDetails_feeRateTooHigh() external { - vm.expectRevert(IEarnerManager.FeeRateTooHigh.selector); - - _earnerManager.setDetails(_alice, true, 10_001); - } - - function test_setDetails_alreadyInRegistrarEarnersList() external { - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - vm.expectRevert(abi.encodeWithSelector(IEarnerManager.AlreadyInRegistrarEarnersList.selector, _alice)); - - _earnerManager.setDetails(_alice, true, 0); - } - - function test_setDetails_earnerDetailsAlreadySet() external { - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); - - vm.expectRevert(abi.encodeWithSelector(IEarnerManager.EarnerDetailsAlreadySet.selector, _alice)); - - vm.prank(_admin2); - _earnerManager.setDetails(_alice, true, 2); - } - - function test_setDetails() external { - vm.prank(_admin1); - _earnerManager.setDetails(_alice, true, 1); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 1); - assertEq(admin_, _admin1); - } - - function test_setDetails_changeFeeRate() external { - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 1); - assertEq(admin_, _admin1); - - vm.prank(_admin1); - _earnerManager.setDetails(_alice, true, 2); - - (status_, feeRate_, admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 2); - assertEq(admin_, _admin1); - } - - function test_setDetails_remove() external { - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 1); - assertEq(admin_, _admin1); - - vm.prank(_admin1); - _earnerManager.setDetails(_alice, false, 0); - - (status_, feeRate_, admin_) = _earnerManager.getEarnerDetails(_alice); - - assertFalse(status_); - assertEq(feeRate_, 0); - assertEq(admin_, address(0)); - } - - /* ============ setEarnerDetails ============ */ - function test_setEarnerDetails_notAdmin() external { - vm.expectRevert(IEarnerManager.NotAdmin.selector); - - vm.prank(_bob); - _earnerManager.setEarnerDetails(_alice, true, 0); - } - - function test_setEarnerDetails_earnersListIgnored() external { - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - - vm.expectRevert(IEarnerManager.EarnersListsIgnored.selector); - - vm.prank(_admin1); - _earnerManager.setEarnerDetails(_alice, true, 0); - } - - function test_setEarnerDetails() external { - vm.expectEmit(); - emit IEarnerManager.EarnerDetailsSet(_alice, true, _admin1, 10_000); - - vm.prank(_admin1); - _earnerManager.setEarnerDetails(_alice, true, 10_000); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 10_000); - assertEq(admin_, _admin1); - } - - /* ============ setEarnerDetails batch ============ */ - function test_setEarnerDetails_batch_notAdmin() external { - vm.expectRevert(IEarnerManager.NotAdmin.selector); - - vm.prank(_alice); - _earnerManager.setEarnerDetails(new address[](0), new bool[](0), new uint16[](0)); - } - - function test_setEarnerDetails_batch_arrayLengthZero() external { - vm.expectRevert(IEarnerManager.ArrayLengthZero.selector); - - vm.prank(_admin1); - _earnerManager.setEarnerDetails(new address[](0), new bool[](2), new uint16[](2)); - } - - function test_setEarnerDetails_batch_arrayLengthMismatch() external { - vm.expectRevert(IEarnerManager.ArrayLengthMismatch.selector); - - vm.prank(_admin1); - _earnerManager.setEarnerDetails(new address[](1), new bool[](2), new uint16[](2)); - - vm.expectRevert(IEarnerManager.ArrayLengthMismatch.selector); - - vm.prank(_admin1); - _earnerManager.setEarnerDetails(new address[](2), new bool[](1), new uint16[](2)); - - vm.expectRevert(IEarnerManager.ArrayLengthMismatch.selector); - - vm.prank(_admin1); - _earnerManager.setEarnerDetails(new address[](2), new bool[](2), new uint16[](1)); - } - - function test_setEarnerDetails_batch_earnersListIgnored() external { - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - - vm.expectRevert(IEarnerManager.EarnersListsIgnored.selector); - - vm.prank(_admin1); - _earnerManager.setEarnerDetails(new address[](2), new bool[](2), new uint16[](2)); - } - - function test_setEarnerDetails_batch() external { - address[] memory accounts_ = new address[](2); - accounts_[0] = _alice; - accounts_[1] = _bob; - - bool[] memory statuses_ = new bool[](2); - statuses_[0] = true; - statuses_[1] = true; - - uint16[] memory feeRates = new uint16[](2); - feeRates[0] = 1; - feeRates[1] = 10_000; - - vm.expectEmit(); - emit IEarnerManager.EarnerDetailsSet(_alice, true, _admin1, 1); - - vm.expectEmit(); - emit IEarnerManager.EarnerDetailsSet(_bob, true, _admin1, 10_000); - - vm.prank(_admin1); - _earnerManager.setEarnerDetails(accounts_, statuses_, feeRates); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 1); - assertEq(admin_, _admin1); - - (status_, feeRate_, admin_) = _earnerManager.getEarnerDetails(_bob); - - assertTrue(status_); - assertEq(feeRate_, 10_000); - assertEq(admin_, _admin1); - } - - /* ============ earnerStatusFor ============ */ - function test_earnerStatusFor_earnersListIgnored() external { - assertFalse(_earnerManager.earnerStatusFor(_alice)); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - - assertTrue(_earnerManager.earnerStatusFor(_alice)); - } - - function test_earnerStatusFor_inEarnersList() external { - assertFalse(_earnerManager.earnerStatusFor(_alice)); - - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - assertTrue(_earnerManager.earnerStatusFor(_alice)); - } - - function test_earnerStatusFor_setByAdmin() external { - assertFalse(_earnerManager.earnerStatusFor(_alice)); - - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); - - assertTrue(_earnerManager.earnerStatusFor(_alice)); - } - - function test_earnerStatusFor_earnersListIgnoredAndInEarnersList() external { - assertFalse(_earnerManager.earnerStatusFor(_alice)); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - assertTrue(_earnerManager.earnerStatusFor(_alice)); - } - - function test_earnerStatusFor_inEarnersListAndSetByAdmin() external { - assertFalse(_earnerManager.earnerStatusFor(_alice)); - - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); - - assertTrue(_earnerManager.earnerStatusFor(_alice)); - } - - function test_earnerStatusFor_earnersListIgnoredAndSetByAdmin() external { - assertFalse(_earnerManager.earnerStatusFor(_alice)); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); - - assertTrue(_earnerManager.earnerStatusFor(_alice)); - } - - function test_earnerStatusFor_earnersListIgnoredAndInEarnersListAndSetByAdmin() external { - assertFalse(_earnerManager.earnerStatusFor(_alice)); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); - - assertTrue(_earnerManager.earnerStatusFor(_alice)); - } - - /* ============ earnerStatusesFor ============ */ - function test_earnerStatusesFor_earnersListIgnored() external { - address[] memory accounts_ = new address[](3); - accounts_[0] = _alice; - accounts_[1] = _bob; - accounts_[2] = _carol; - - bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertFalse(statuses_[0]); - assertFalse(statuses_[1]); - assertFalse(statuses_[2]); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - - statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertTrue(statuses_[0]); - assertTrue(statuses_[1]); - assertTrue(statuses_[2]); - } - - function test_earnerStatusesFor_inEarnersList() external { - address[] memory accounts_ = new address[](3); - accounts_[0] = _alice; - accounts_[1] = _bob; - accounts_[2] = _carol; - - bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertFalse(statuses_[0]); - assertFalse(statuses_[1]); - assertFalse(statuses_[2]); - - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertTrue(statuses_[0]); - assertFalse(statuses_[1]); - assertFalse(statuses_[2]); - } - - function test_earnerStatusesFor_setByAdmin() external { - address[] memory accounts_ = new address[](3); - accounts_[0] = _alice; - accounts_[1] = _bob; - accounts_[2] = _carol; - - bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertFalse(statuses_[0]); - assertFalse(statuses_[1]); - assertFalse(statuses_[2]); - - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); - _earnerManager.setInternalEarnerDetails(_bob, _admin2, 0); - - statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertTrue(statuses_[0]); - assertTrue(statuses_[1]); - assertFalse(statuses_[2]); - } - - function test_earnerStatusesFor_earnersListIgnoredAndInEarnersList() external { - address[] memory accounts_ = new address[](3); - accounts_[0] = _alice; - accounts_[1] = _bob; - accounts_[2] = _carol; - - bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertFalse(statuses_[0]); - assertFalse(statuses_[1]); - assertFalse(statuses_[2]); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertTrue(statuses_[0]); - assertTrue(statuses_[1]); - assertTrue(statuses_[2]); - } - - function test_earnerStatusesFor_inEarnersListAndSetByAdmin() external { - address[] memory accounts_ = new address[](3); - accounts_[0] = _alice; - accounts_[1] = _bob; - accounts_[2] = _carol; - - bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertFalse(statuses_[0]); - assertFalse(statuses_[1]); - assertFalse(statuses_[2]); - - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); - _earnerManager.setInternalEarnerDetails(_bob, _admin1, 0); - _earnerManager.setInternalEarnerDetails(_carol, _admin2, 0); - - statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertTrue(statuses_[0]); - assertTrue(statuses_[1]); - assertTrue(statuses_[2]); - } - - function test_earnerStatusesFor_earnersListIgnoredAndSetByAdmin() external { - address[] memory accounts_ = new address[](3); - accounts_[0] = _alice; - accounts_[1] = _bob; - accounts_[2] = _carol; - - bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertFalse(statuses_[0]); - assertFalse(statuses_[1]); - assertFalse(statuses_[2]); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); - _earnerManager.setInternalEarnerDetails(_bob, _admin2, 0); - - statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertTrue(statuses_[0]); - assertTrue(statuses_[1]); - assertTrue(statuses_[2]); - } - - function test_earnerStatusesFor_earnersListIgnoredAndInEarnersListAndSetByAdmin() external { - address[] memory accounts_ = new address[](3); - accounts_[0] = _alice; - accounts_[1] = _bob; - accounts_[2] = _carol; - - bool[] memory statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertFalse(statuses_[0]); - assertFalse(statuses_[1]); - assertFalse(statuses_[2]); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); - _earnerManager.setInternalEarnerDetails(_bob, _admin1, 0); - _earnerManager.setInternalEarnerDetails(_carol, _admin2, 0); - - statuses_ = _earnerManager.earnerStatusesFor(accounts_); - - assertTrue(statuses_[0]); - assertTrue(statuses_[1]); - assertTrue(statuses_[2]); - } - - /* ============ earnersListsIgnored ============ */ - function test_earnersListsIgnored() external { - assertFalse(_earnerManager.earnersListsIgnored()); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - - assertTrue(_earnerManager.earnersListsIgnored()); - } - - /* ============ isInRegistrarEarnersList ============ */ - function test_isInRegistrarEarnersList() external { - assertFalse(_earnerManager.isInRegistrarEarnersList(_alice)); - - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - assertTrue(_earnerManager.isInRegistrarEarnersList(_alice)); - } - - /* ============ isInAdministratedEarnersList ============ */ - function test_isInAdministratedEarnersList() external { - assertFalse(_earnerManager.isInAdministratedEarnersList(_alice)); - - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 0); - - assertTrue(_earnerManager.isInAdministratedEarnersList(_alice)); - } - - /* ============ getEarnerDetails ============ */ - function test_getEarnerDetails_earnersListIgnored() external { - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); - - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 0); - assertEq(admin_, address(0)); - } - - function test_getEarnerDetails_inEarnersList() external { - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); - - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 0); - assertEq(admin_, address(0)); - } - - function test_getEarnerDetails_invalidAdmin() external { - _earnerManager.setInternalEarnerDetails(_alice, _bob, 1); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertFalse(status_); - assertEq(feeRate_, 0); - assertEq(admin_, address(0)); - } - - function test_getEarnerDetails() external { - _earnerManager.setInternalEarnerDetails(_alice, _admin1, 1); - - (bool status_, uint16 feeRate_, address admin_) = _earnerManager.getEarnerDetails(_alice); - - assertTrue(status_); - assertEq(feeRate_, 1); - assertEq(admin_, _admin1); - } - - /* ============ getEarnerDetails batch ============ */ - function test_getEarnerDetails_batch_earnersListIgnored() external { - _registrar.set(_EARNERS_LIST_IGNORED_KEY, bytes32(uint256(1))); - - address[] memory accounts_ = new address[](2); - accounts_[0] = _alice; - accounts_[1] = _bob; - - (bool[] memory statuses_, uint16[] memory feeRates_, address[] memory admins_) = _earnerManager - .getEarnerDetails(accounts_); - - assertTrue(statuses_[0]); - assertEq(feeRates_[0], 0); - assertEq(admins_[0], address(0)); - - assertTrue(statuses_[1]); - assertEq(feeRates_[1], 0); - assertEq(admins_[1], address(0)); - } - - function test_getEarnerDetails_batch() external { - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - _earnerManager.setInternalEarnerDetails(_bob, _admin1, 1); - _earnerManager.setInternalEarnerDetails(_carol, _frank, 2); // Invalid admin - - address[] memory accounts_ = new address[](4); - accounts_[0] = _alice; - accounts_[1] = _bob; - accounts_[2] = _carol; - accounts_[3] = _dave; - - (bool[] memory statuses_, uint16[] memory feeRates_, address[] memory admins_) = _earnerManager - .getEarnerDetails(accounts_); - - assertTrue(statuses_[0]); - assertEq(feeRates_[0], 0); - assertEq(admins_[0], address(0)); - - assertTrue(statuses_[1]); - assertEq(feeRates_[1], 1); - assertEq(admins_[1], _admin1); - - assertFalse(statuses_[2]); - assertEq(feeRates_[2], 0); - assertEq(admins_[2], address(0)); - - assertFalse(statuses_[3]); - assertEq(feeRates_[3], 0); - assertEq(admins_[3], address(0)); - } - - /* ============ isAdmin ============ */ - function test_isAdmin() external view { - assertFalse(_earnerManager.isAdmin(_alice)); - assertTrue(_earnerManager.isAdmin(_admin1)); - assertTrue(_earnerManager.isAdmin(_admin2)); - } -} diff --git a/test/unit/Migrations.t.sol b/test/unit/Migrations.t.sol index b97711d..c62ceb2 100644 --- a/test/unit/Migrations.t.sol +++ b/test/unit/Migrations.t.sol @@ -5,10 +5,8 @@ pragma solidity 0.8.26; import { Proxy } from "../../lib/common/src/Proxy.sol"; import { Test } from "../../lib/forge-std/src/Test.sol"; -import { IEarnerManager } from "../../src/interfaces/IEarnerManager.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { EarnerManager } from "../../src/EarnerManager.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; import { WrappedMTokenMigratorV1 as WrappedMTokenMigrator } from "../../src/WrappedMTokenMigratorV1.sol"; @@ -20,24 +18,6 @@ contract Foo { } } -contract EarnerManagerMigrator { - uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - address public immutable implementationV2; - - constructor(address implementation_) { - implementationV2 = implementation_; - } - - fallback() external virtual { - address implementation_ = implementationV2; - - assembly { - sstore(_IMPLEMENTATION_SLOT, implementation_) - } - } -} - contract MigrationTests is Test { bytes32 internal constant _WM_MIGRATOR_KEY_PREFIX = "wm_migrator_v2"; bytes32 internal constant _EM_MIGRATOR_KEY_PREFIX = "em_migrator_v1"; @@ -48,7 +28,6 @@ contract MigrationTests is Test { address internal _dave = makeAddr("dave"); address internal _mToken = makeAddr("mToken"); - address internal _earnerManager = makeAddr("earnerManager"); address internal _excessDestination = makeAddr("excessDestination"); address internal _swapFacility = makeAddr("swapFacility"); address internal _migrationAdmin = makeAddr("migrationAdmin"); @@ -58,14 +37,7 @@ contract MigrationTests is Test { address mToken_ = makeAddr("mToken"); address implementation_ = address( - new WrappedMToken( - address(mToken_), - address(registrar_), - _earnerManager, - _excessDestination, - _swapFacility, - _migrationAdmin - ) + new WrappedMToken(address(mToken_), address(registrar_), _excessDestination, _swapFacility, _migrationAdmin) ); address proxy_ = address(new Proxy(address(implementation_))); @@ -86,14 +58,7 @@ contract MigrationTests is Test { address mToken_ = makeAddr("mToken"); address implementation_ = address( - new WrappedMToken( - address(mToken_), - address(registrar_), - _earnerManager, - _excessDestination, - _swapFacility, - _migrationAdmin - ) + new WrappedMToken(address(mToken_), address(registrar_), _excessDestination, _swapFacility, _migrationAdmin) ); address proxy_ = address(new Proxy(address(implementation_))); @@ -107,37 +72,4 @@ contract MigrationTests is Test { assertEq(Foo(proxy_).bar(), 1); } - - function test_earnerManager_migration() external { - MockRegistrar registrar_ = new MockRegistrar(); - - address implementation_ = address(new EarnerManager(address(registrar_), _migrationAdmin)); - address proxy_ = address(new Proxy(address(implementation_))); - address migrator_ = address(new EarnerManagerMigrator(address(new Foo()))); - - registrar_.set(keccak256(abi.encode(_EM_MIGRATOR_KEY_PREFIX, proxy_)), bytes32(uint256(uint160(migrator_)))); - - vm.expectRevert(); - Foo(proxy_).bar(); - - IWrappedMToken(proxy_).migrate(); - - assertEq(Foo(proxy_).bar(), 1); - } - - function test_earnerManager_migration_fromAdmin() external { - MockRegistrar registrar_ = new MockRegistrar(); - - address implementation_ = address(new EarnerManager(address(registrar_), _migrationAdmin)); - address proxy_ = address(new Proxy(address(implementation_))); - address migrator_ = address(new EarnerManagerMigrator(address(new Foo()))); - - vm.expectRevert(); - Foo(proxy_).bar(); - - vm.prank(_migrationAdmin); - IEarnerManager(proxy_).migrate(migrator_); - - assertEq(Foo(proxy_).bar(), 1); - } } diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index 95e8c87..ab16532 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -11,7 +11,7 @@ import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; -import { MockEarnerManager, MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; +import { MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; contract StoryTests is Test { uint56 internal constant _EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; @@ -26,7 +26,6 @@ contract StoryTests is Test { address internal _excessDestination = makeAddr("excessDestination"); address internal _migrationAdmin = makeAddr("migrationAdmin"); - MockEarnerManager internal _earnerManager; MockM internal _mToken; MockRegistrar internal _registrar; MockSwapFacility internal _swapFacility; @@ -40,12 +39,9 @@ contract StoryTests is Test { _mToken.setCurrentIndex(_EXP_SCALED_ONE); _swapFacility = new MockSwapFacility(address(_mToken)); - _earnerManager = new MockEarnerManager(); - _implementation = new WrappedMToken( address(_mToken), address(_registrar), - address(_earnerManager), _excessDestination, address(_swapFacility), _migrationAdmin @@ -55,8 +51,8 @@ contract StoryTests is Test { } function test_story() external { - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); - _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _wrappedMToken.enableEarning(); @@ -253,7 +249,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 283_333336); assertEq(_wrappedMToken.excess(), 416_666664); - _earnerManager.setEarnerDetails(_alice, false, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, false); _wrappedMToken.stopEarningFor(_alice); @@ -268,7 +264,7 @@ contract StoryTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 116_666672); assertEq(_wrappedMToken.excess(), 416_666664); - _earnerManager.setEarnerDetails(_carol, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); _wrappedMToken.startEarningFor(_carol); @@ -379,8 +375,8 @@ contract StoryTests is Test { } function test_noExcessCreep() external { - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); - _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); + _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); @@ -422,8 +418,8 @@ contract StoryTests is Test { } function test_dustWrapping() external { - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); - _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); + _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); diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 6de11d2..993bccc 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -2,8 +2,6 @@ pragma solidity 0.8.26; -import { console2 } from "../../lib/forge-std/src/Test.sol"; - import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; import { UIntMath } from "../../lib/common/src/libs/UIntMath.sol"; @@ -15,7 +13,7 @@ import { Test } from "../../lib/forge-std/src/Test.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { MockEarnerManager, MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; +import { MockM, MockRegistrar, MockSwapFacility } from "../utils/Mocks.sol"; import { WrappedMTokenHarness } from "../utils/WrappedMTokenHarness.sol"; // TODO: All operations involving earners should include demonstration of accrued yield being added to their balance. @@ -40,7 +38,6 @@ contract WrappedMTokenTests is Test { address[] internal _accounts = [_alice, _bob, _charlie, _david]; - MockEarnerManager internal _earnerManager; MockM internal _mToken; MockRegistrar internal _registrar; MockSwapFacility internal _swapFacility; @@ -52,14 +49,11 @@ contract WrappedMTokenTests is Test { _mToken = new MockM(); - _earnerManager = new MockEarnerManager(); - _swapFacility = new MockSwapFacility(address(_mToken)); _implementation = new WrappedMTokenHarness( address(_mToken), address(_registrar), - address(_earnerManager), _excessDestination, address(_swapFacility), _migrationAdmin @@ -93,41 +87,22 @@ contract WrappedMTokenTests is Test { function test_constructor_zeroMToken() external { vm.expectRevert(IWrappedMToken.ZeroMToken.selector); - new WrappedMTokenHarness(address(0), address(0), address(0), address(0), address(0), address(0)); + new WrappedMTokenHarness(address(0), 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), address(0), address(0)); - } - - function test_constructor_zeroEarnerManager() external { - vm.expectRevert(IWrappedMToken.ZeroEarnerManager.selector); - new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0), address(0), address(0)); + new WrappedMTokenHarness(address(_mToken), address(0), address(0), address(0), address(0)); } function test_constructor_zeroExcessDestination() external { vm.expectRevert(IWrappedMToken.ZeroExcessDestination.selector); - new WrappedMTokenHarness( - address(_mToken), - address(_registrar), - address(_earnerManager), - address(0), - address(0), - address(0) - ); + new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0), address(0)); } function test_constructor_zeroSwapFacility() external { vm.expectRevert(IWrappedMToken.ZeroSwapFacility.selector); - new WrappedMTokenHarness( - address(_mToken), - address(_registrar), - address(_earnerManager), - _excessDestination, - address(0), - address(0) - ); + new WrappedMTokenHarness(address(_mToken), address(_registrar), _excessDestination, address(0), address(0)); } function test_constructor_zeroMigrationAdmin() external { @@ -135,7 +110,6 @@ contract WrappedMTokenTests is Test { new WrappedMTokenHarness( address(_mToken), address(_registrar), - address(_earnerManager), _excessDestination, address(_swapFacility), address(0) @@ -203,7 +177,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _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); @@ -355,7 +329,7 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setAccountOf(_alice, 999, 909, false, false); + _wrappedMToken.setAccountOf(_alice, 999, 909, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); vm.prank(_alice); @@ -434,7 +408,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _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); @@ -580,7 +554,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -613,7 +587,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -641,87 +615,6 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 1); } - function test_claimFor_earner_withFee() external { - _mToken.setCurrentIndex(1_210000000000); - _wrappedMToken.setEnableMIndex(1_100000000000); - - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, true); - _earnerManager.setEarnerDetails(_alice, true, 1_500, _bob); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - - vm.expectEmit(); - emit IWrappedMToken.Claimed(_alice, _alice, 100); - - vm.expectEmit(); - emit IERC20.Transfer(address(0), _alice, 100); - - vm.expectEmit(); - emit IERC20.Transfer(_alice, _bob, 15); - - assertEq(_wrappedMToken.claimFor(_alice), 100); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_085); - assertEq(_wrappedMToken.balanceOf(_bob), 15); - } - - function test_claimFor_earner_withFeeAboveOneHundredPercent() external { - _mToken.setCurrentIndex(1_210000000000); - _wrappedMToken.setEnableMIndex(1_100000000000); - - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, true); - _earnerManager.setEarnerDetails(_alice, true, type(uint16).max, _bob); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - - vm.expectEmit(); - emit IWrappedMToken.Claimed(_alice, _alice, 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.balanceOf(_alice), 1_000); - assertEq(_wrappedMToken.balanceOf(_bob), 100); - } - - function test_claimFor_earner_withOverrideRecipientAndFee() external { - _mToken.setCurrentIndex(1_210000000000); - _wrappedMToken.setEnableMIndex(1_100000000000); - - _registrar.set( - keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), - bytes32(uint256(uint160(_charlie))) - ); - - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, true); - _earnerManager.setEarnerDetails(_alice, true, 1_500, _bob); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - - vm.expectEmit(); - emit IWrappedMToken.Claimed(_alice, _charlie, 100); - - vm.expectEmit(); - emit IERC20.Transfer(address(0), _alice, 100); - - vm.expectEmit(); - emit IERC20.Transfer(_alice, _bob, 15); - - vm.expectEmit(); - emit IERC20.Transfer(_alice, _charlie, 85); - - assertEq(_wrappedMToken.claimFor(_alice), 100); - - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - assertEq(_wrappedMToken.balanceOf(_bob), 15); - assertEq(_wrappedMToken.balanceOf(_charlie), 85); - } - function testFuzz_claimFor( bool earningEnabled_, bool accountEarning_, @@ -730,8 +623,7 @@ contract WrappedMTokenTests is Test { uint128 currentMIndex_, uint128 enableMIndex_, uint128 disableIndex_, - bool claimOverride_, - uint16 feeRate_ + bool claimOverride_ ) external { (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( currentMIndex_, @@ -749,11 +641,6 @@ contract WrappedMTokenTests is Test { _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); - if (feeRate_ != 0) { - _wrappedMToken.setHasEarnerDetails(_alice, true); - _earnerManager.setEarnerDetails(_alice, true, feeRate_, _bob); - } - if (claimOverride_) { _registrar.set( keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), @@ -771,17 +658,9 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(address(0), _alice, accruedYield_); } - uint240 fee_ = (accruedYield_ * (feeRate_ > _ONE_HUNDRED_PERCENT ? _ONE_HUNDRED_PERCENT : feeRate_)) / - _ONE_HUNDRED_PERCENT; - - if (fee_ != 0) { - vm.expectEmit(); - emit IERC20.Transfer(_alice, _bob, fee_); - } - - if (claimOverride_ && (accruedYield_ - fee_ != 0)) { + if (claimOverride_ && (accruedYield_ != 0)) { vm.expectEmit(); - emit IERC20.Transfer(_alice, _charlie, accruedYield_ - fee_); + emit IERC20.Transfer(_alice, _charlie, accruedYield_); } assertEq(_wrappedMToken.claimFor(_alice), accruedYield_); @@ -881,7 +760,7 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 1_000, 1_001)); vm.prank(_alice); @@ -947,7 +826,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalNonEarningSupply(500); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. _wrappedMToken.setAccountOf(_bob, 500); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -997,7 +876,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalNonEarningSupply(1_000); _wrappedMToken.setAccountOf(_alice, 1_000); - _wrappedMToken.setAccountOf(_bob, 500, 500, false, false); // 550 balance with yield. + _wrappedMToken.setAccountOf(_bob, 500, 500, false); // 550 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); @@ -1026,8 +905,8 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_500); _wrappedMToken.setTotalEarningSupply(1_500); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. - _wrappedMToken.setAccountOf(_bob, 500, 500, false, false); // 550 balance with yield. + _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); @@ -1075,7 +954,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -1188,7 +1067,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, aliceBalance_); - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); vm.expectRevert(UIntMath.InvalidUInt112.selector); _wrappedMToken.startEarningFor(_alice); @@ -1202,7 +1081,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000); - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); vm.expectEmit(); emit IWrappedMToken.StartedEarning(_alice); @@ -1238,7 +1117,7 @@ contract WrappedMTokenTests is Test { _setupAccount(_alice, false, 0, balance_); - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); vm.expectEmit(); emit IWrappedMToken.StartedEarning(_alice); @@ -1267,7 +1146,7 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); address[] memory accounts_ = new address[](2); accounts_[0] = _alice; @@ -1281,8 +1160,8 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); - _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); address[] memory accounts_ = new address[](2); accounts_[0] = _alice; @@ -1299,7 +1178,7 @@ contract WrappedMTokenTests is Test { /* ============ stopEarningFor ============ */ function test_stopEarningFor_isApprovedEarner() external { - _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.IsApprovedEarner.selector, _alice)); _wrappedMToken.stopEarningFor(_alice); @@ -1312,7 +1191,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -1390,7 +1269,7 @@ contract WrappedMTokenTests is Test { /* ============ setClaimRecipient ============ */ function test_setClaimRecipient() external { - (, , , bool hasClaimRecipient_, ) = _wrappedMToken.getAccountOf(_alice); + (, , , bool hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); assertFalse(hasClaimRecipient_); assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), address(0)); @@ -1398,7 +1277,7 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.setClaimRecipient(_alice); - (, , , hasClaimRecipient_, ) = _wrappedMToken.getAccountOf(_alice); + (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); assertTrue(hasClaimRecipient_); assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), _alice); @@ -1406,7 +1285,7 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.setClaimRecipient(_bob); - (, , , hasClaimRecipient_, ) = _wrappedMToken.getAccountOf(_alice); + (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); assertTrue(hasClaimRecipient_); assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), _bob); @@ -1414,7 +1293,7 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.setClaimRecipient(address(0)); - (, , , hasClaimRecipient_, ) = _wrappedMToken.getAccountOf(_alice); + (, , , hasClaimRecipient_) = _wrappedMToken.getAccountOf(_alice); assertFalse(hasClaimRecipient_); assertEq(_wrappedMToken.getInternalClaimRecipientOf(_alice), address(0)); @@ -1422,7 +1301,7 @@ contract WrappedMTokenTests is Test { /* ============ stopEarningFor batch ============ */ function test_stopEarningFor_batch_isApprovedEarner() external { - _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); + _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); address[] memory accounts_ = new address[](2); accounts_[0] = _alice; @@ -1433,8 +1312,8 @@ contract WrappedMTokenTests is Test { } function test_stopEarningFor_batch() external { - _wrappedMToken.setAccountOf(_alice, 0, 0, false, false); - _wrappedMToken.setAccountOf(_bob, 0, 0, false, false); + _wrappedMToken.setAccountOf(_alice, 0, 0, false); + _wrappedMToken.setAccountOf(_bob, 0, 0, false); address[] memory accounts_ = new address[](2); accounts_[0] = _alice; @@ -1525,7 +1404,7 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 500); @@ -1533,7 +1412,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -1541,7 +1420,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false, false); // 1_815 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } @@ -1568,11 +1447,11 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 550); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); @@ -1580,7 +1459,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_210); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false, false); // 1_815 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false); // 1_815 balance with yield. assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_815); } @@ -1607,11 +1486,11 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setAccountOf(_alice, 500, 500, false, false); // 550 balance with yield. + _wrappedMToken.setAccountOf(_alice, 500, 500, false); // 550 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 50); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false, false); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -1619,18 +1498,18 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 210); - _wrappedMToken.setAccountOf(_alice, 1_000, 1_500, false, false); // 1_815 balance with yield. + _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, false); + _wrappedMToken.setAccountOf(_alice, 0, 100, false); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 100); - _wrappedMToken.setAccountOf(_alice, 0, 200, false, false); + _wrappedMToken.setAccountOf(_alice, 0, 200, false); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 200); } @@ -1641,7 +1520,7 @@ contract WrappedMTokenTests is Test { assertFalse(_wrappedMToken.isEarning(_alice)); - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false, false); + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); assertTrue(_wrappedMToken.isEarning(_alice)); } @@ -1669,7 +1548,7 @@ contract WrappedMTokenTests is Test { } function test_claimRecipientFor_hasClaimRecipient() external { - _wrappedMToken.setAccountOf(_alice, 0, 0, true, false); + _wrappedMToken.setAccountOf(_alice, 0, 0, true); _wrappedMToken.setInternalClaimRecipient(_alice, _bob); assertEq(_wrappedMToken.claimRecipientFor(_alice), _bob); @@ -1685,7 +1564,7 @@ contract WrappedMTokenTests is Test { } function test_claimRecipientFor_hasClaimRecipientAndOverrideRecipient() external { - _wrappedMToken.setAccountOf(_alice, 0, 0, true, false); + _wrappedMToken.setAccountOf(_alice, 0, 0, true); _wrappedMToken.setInternalClaimRecipient(_alice, _bob); _registrar.set( @@ -1943,7 +1822,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.currentIndex() ); - _wrappedMToken.setAccountOf(account_, balance_, principal_, false, false); + _wrappedMToken.setAccountOf(account_, balance_, principal_, false); _wrappedMToken.setTotalEarningPrincipal(_wrappedMToken.totalEarningPrincipal() + principal_); _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + balance_); } else { diff --git a/test/utils/EarnerManagerHarness.sol b/test/utils/EarnerManagerHarness.sol deleted file mode 100644 index fb2e798..0000000 --- a/test/utils/EarnerManagerHarness.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity 0.8.26; - -import { EarnerManager } from "../../src/EarnerManager.sol"; - -contract EarnerManagerHarness is EarnerManager { - constructor(address registrar_, address migrationAdmin_) EarnerManager(registrar_, migrationAdmin_) {} - - function setInternalEarnerDetails(address account_, address admin_, uint16 feeRate_) external { - _earnerDetails[account_] = EarnerDetails(admin_, feeRate_); - } - - function setDetails(address account_, bool status_, uint16 feeRate_) external { - _setDetails(account_, status_, feeRate_); - } -} diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index 4e50749..afe6242 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -86,26 +86,6 @@ contract MockRegistrar { } } -contract MockEarnerManager { - struct EarnerDetails { - bool status; - uint16 feeRate; - address admin; - } - - mapping(address account => EarnerDetails earnerDetails) internal _earnerDetails; - - function setEarnerDetails(address account_, bool status_, uint16 feeRate_, address admin_) external { - _earnerDetails[account_] = EarnerDetails(status_, feeRate_, admin_); - } - - function getEarnerDetails(address account_) external view returns (bool status_, uint16 feeRate_, address admin_) { - EarnerDetails storage earnerDetails_ = _earnerDetails[account_]; - - return (earnerDetails_.status, earnerDetails_.feeRate, earnerDetails_.admin); - } -} - interface IMExtension { function wrap(address recipient, uint256 amount) external; function unwrap(address recipient, uint256 amount) external; diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index edae0f6..feaf54e 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -8,11 +8,10 @@ contract WrappedMTokenHarness is WrappedMToken { constructor( address mToken_, address registrar_, - address earnerManager_, address excessDestination_, address swapFacility_, address migrationAdmin_ - ) WrappedMToken(mToken_, registrar_, earnerManager_, excessDestination_, swapFacility_, migrationAdmin_) {} + ) WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, migrationAdmin_) {} function internalWrap(address recipient_, uint240 amount_) external { _wrap(recipient_, amount_); @@ -34,20 +33,13 @@ contract WrappedMTokenHarness is WrappedMToken { address account_, uint256 balance_, uint256 earningPrincipal_, - bool hasClaimRecipient_, - bool hasEarnerDetails_ + bool hasClaimRecipient_ ) external { - _accounts[account_] = Account( - true, - uint240(balance_), - uint112(earningPrincipal_), - hasClaimRecipient_, - hasEarnerDetails_ - ); + _accounts[account_] = Account(true, uint240(balance_), uint112(earningPrincipal_), hasClaimRecipient_); } function setAccountOf(address account_, uint256 balance_) external { - _accounts[account_] = Account(false, uint240(balance_), 0, false, false); + _accounts[account_] = Account(false, uint240(balance_), 0, false); } function setInternalClaimRecipient(address account_, address claimRecipient_) external { @@ -74,31 +66,11 @@ contract WrappedMTokenHarness is WrappedMToken { disableIndex = uint128(disableIndex_); } - function setHasEarnerDetails(address account_, bool hasEarnerDetails_) external { - _accounts[account_].hasEarnerDetails = hasEarnerDetails_; - } - function getAccountOf( address account_ - ) - external - view - returns ( - bool isEarning_, - uint240 balance_, - uint112 earningPrincipal_, - bool hasClaimRecipient_, - bool hasEarnerDetails_ - ) - { + ) 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, - account.hasEarnerDetails - ); + return (account.isEarning, account.balance, account.earningPrincipal, account.hasClaimRecipient); } function getInternalClaimRecipientOf(address account_) external view returns (address claimRecipient_) { From 5e93dfa59928fc3350a0b9ec364bd9ac992f0b1f Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 16 Dec 2025 04:22:29 -0600 Subject: [PATCH 17/27] feat(WrappedM): add Pausable and Freezable (#125) * chore(lib): update libs * feat(WrappedM): add Freezable * feat(WrappedM): add Pausable * feat(WrappedM): add initialize upgrade pattern * fix(build): reduce optimizer runs * chore(lib): import Pausable and Freezable from extensions repo --- .gitmodules | 8 +- foundry.lock | 17 + foundry.toml | 5 +- lib/common | 2 +- lib/evm-m-extensions | 1 + lib/forge-std | 2 +- script/DeployBase.sol | 13 +- script/DeployUpgradeMainnet.s.sol | 14 +- src/WrappedMToken.sol | 77 +++- src/WrappedMTokenMigratorV1.sol | 27 +- src/interfaces/ISwapFacilityLike.sol | 11 + src/interfaces/IWrappedMToken.sol | 3 + .../WrappedMTokenHarness.sol | 8 +- test/integration/TestBase.sol | 7 +- test/integration/Upgrade.t.sol | 53 ++- test/unit/Migrations.t.sol | 30 +- test/unit/Stories.t.sol | 67 +++ test/unit/WrappedMToken.t.sol | 433 +++++++++++++++--- test/utils/BaseUnitTest.sol | 41 ++ test/utils/Mocks.sol | 2 +- 20 files changed, 731 insertions(+), 90 deletions(-) create mode 100644 foundry.lock create mode 160000 lib/evm-m-extensions rename test/{utils => harness}/WrappedMTokenHarness.sol (91%) create mode 100644 test/utils/BaseUnitTest.sol diff --git a/.gitmodules b/.gitmodules index cabe015..cfad28d 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.4.0 +[submodule "lib/evm-m-extensions"] + path = lib/evm-m-extensions + url = https://github.com/m0-foundation/evm-m-extensions diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..317c342 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,17 @@ +{ + "lib/common": { + "branch": { + "name": "release-v1.4.0", + "rev": "613d2d95a476612270324ecbe92317eb5e9bc98f" + } + }, + "lib/evm-m-extensions": { + "rev": "f85daca886802be8e170f84dbb45cede6f460a20" + }, + "lib/forge-std": { + "branch": { + "name": "v1", + "rev": "7117c90c8cf6c68e5acce4f09a6b24715cea4de6" + } + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 69a589d..a333706 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,12 +4,15 @@ gas_reports_ignore = [] ignored_error_codes = [] solc_version = "0.8.26" optimizer = true -optimizer_runs = 999999 +optimizer_runs = 22099 verbosity = 3 fork_block_number = 23_170_985 rpc_storage_caching = { chains = ["mainnet"], endpoints = "all" } evm_version = "cancun" +[lint] +lint_on_build = false + [profile.production] build_info = true sizes = true diff --git a/lib/common b/lib/common index 36b3cc9..613d2d9 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 36b3cc900dba907bafa2ca3f9d2fc9c00fabe805 +Subproject commit 613d2d95a476612270324ecbe92317eb5e9bc98f diff --git a/lib/evm-m-extensions b/lib/evm-m-extensions new file mode 160000 index 0000000..f85daca --- /dev/null +++ b/lib/evm-m-extensions @@ -0,0 +1 @@ +Subproject commit f85daca886802be8e170f84dbb45cede6f460a20 diff --git a/lib/forge-std b/lib/forge-std index b93cf4b..7117c90 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit b93cf4bc34ff214c099dc970b153f85ade8c9f66 +Subproject commit 7117c90c8cf6c68e5acce4f09a6b24715cea4de6 diff --git a/script/DeployBase.sol b/script/DeployBase.sol index 4e15a9e..56512f4 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -40,6 +40,10 @@ contract DeployBase { * @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 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. * @return wrappedMTokenImplementation_ The address of the deployed Wrapped M Token implementation. * @return wrappedMTokenMigrator_ The address of the deployed Wrapped M Token Migrator. */ @@ -49,13 +53,18 @@ contract DeployBase { address excessDestination_, address swapFacility_, address wrappedMMigrationAdmin_, - address[] memory earners_ + address[] memory earners_, + address admin_, + address freezeManager_, + address pauser_ ) public virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) { wrappedMTokenImplementation_ = address( new WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) ); - wrappedMTokenMigrator_ = address(new WrappedMTokenMigratorV1(wrappedMTokenImplementation_, earners_)); + wrappedMTokenMigrator_ = address( + new WrappedMTokenMigratorV1(wrappedMTokenImplementation_, earners_, admin_, freezeManager_, pauser_) + ); } /** diff --git a/script/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol index 548ab50..f3d19b7 100644 --- a/script/DeployUpgradeMainnet.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -34,6 +34,15 @@ contract DeployUpgradeMainnet is Script, DeployBase { 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 mainnet deployer to use. address internal constant _EXPECTED_DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; @@ -129,7 +138,10 @@ contract DeployUpgradeMainnet is Script, DeployBase { _EXCESS_DESTINATION, _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, - earners_ + earners_, + _ADMIN, + _FREEZE_MANAGER, + _PAUSER ); vm.stopBroadcast(); diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 156dd81..1e95533 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -5,6 +5,9 @@ pragma solidity 0.8.26; import { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol"; import { UIntMath } from "../lib/common/src/libs/UIntMath.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"; @@ -12,6 +15,7 @@ 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"; /* @@ -29,7 +33,7 @@ import { IWrappedMToken } from "./interfaces/IWrappedMToken.sol"; * @title ERC20 Token contract for wrapping M into a non-rebasing token with claimable yields. * @author M0 Labs */ -contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { +contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, Pausable { /* ============ Structs ============ */ /** @@ -133,16 +137,37 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); } + /* ============ Initializer ============ */ + + /** + * @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. + */ + function initialize(address admin_, address freezeManager_, address pauser_) public initializer { + if (admin_ == address(0)) revert ZeroAdmin(); + _grantRole(DEFAULT_ADMIN_ROLE, admin_); + + __Freezable_init(freezeManager_); + __Pausable_init(pauser_); + } + /* ============ Interactive Functions ============ */ /// @inheritdoc IWrappedMToken function wrap(address recipient_, uint256 amount_) external onlySwapFacility { - _wrap(recipient_, UIntMath.safe240(amount_)); + // 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_ */, uint256 amount_) external onlySwapFacility { - _unwrap(UIntMath.safe240(amount_)); + // 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 @@ -152,6 +177,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function claimExcess() external returns (uint240 claimed_) { + _requireNotPaused(); + int256 excess_ = excess(); if (excess_ <= 0) return 0; @@ -331,6 +358,21 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /* ============ Internal Interactive Functions ============ */ + /** + * @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. @@ -450,6 +492,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @return yield_ The accrued yield that was claimed. */ function _claim(address account_, uint128 currentIndex_) internal returns (uint240 yield_) { + _requireNotPaused(); + _revertIfFrozen(account_); + Account storage accountInfo_ = _accounts[account_]; if (!accountInfo_.isEarning) return 0; @@ -485,8 +530,15 @@ 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_); + FreezableStorageStruct storage $ = _getFreezableStorageLocation(); + + _revertIfFrozen($, msg.sender); + _revertIfFrozen($, sender_); + _revertIfFrozen($, recipient_); + emit Transfer(sender_, recipient_, amount_); if (amount_ == 0) return; @@ -604,10 +656,18 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /** * @dev Wraps `amount` M from `account_` into wM for `recipient`. + * @param account_ The account depositing M. * @param recipient_ The account receiving the minted wM. * @param amount_ The amount of M deposited. */ - function _wrap(address recipient_, uint240 amount_) internal { + 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(msg.sender, address(this), amount_); @@ -622,9 +682,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /** * @dev Unwraps `amount` wM from `account_` into M. - * @param amount_ The amount of wM burned. + * @param account_ The account whose wM is being burned. + * @param amount_ The amount of wM burned. */ - function _unwrap(uint240 amount_) internal { + function _unwrap(address account_, uint240 amount_) internal { + _requireNotPaused(); + _revertIfFrozen(account_); + // NOTE: Always burn from SwapFacility as it is the only contract that can call this function. _burn(msg.sender, amount_); @@ -642,6 +706,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param currentIndex_ The current index. */ function _startEarningFor(address account_, uint128 currentIndex_) internal { + _requireNotPaused(); _revertIfNotApprovedEarner(account_); Account storage accountInfo_ = _accounts[account_]; diff --git a/src/WrappedMTokenMigratorV1.sol b/src/WrappedMTokenMigratorV1.sol index 5574543..62e2157 100644 --- a/src/WrappedMTokenMigratorV1.sol +++ b/src/WrappedMTokenMigratorV1.sol @@ -51,10 +51,24 @@ contract WrappedMTokenMigratorV1 { address public immutable listOfEarnerToMigrate; - constructor(address newImplementation_, address[] memory earners_) { + address public immutable admin; + address public immutable freezeManager; + address public immutable pauser; + + constructor( + address newImplementation_, + address[] memory earners_, + address admin_, + address freezeManager_, + address pauser_ + ) { newImplementation = newImplementation_; listOfEarnerToMigrate = address(new ListOfEarnersToMigrate(earners_)); + + admin = admin_; + freezeManager = freezeManager_; + pauser = pauser_; } fallback() external virtual { @@ -73,6 +87,17 @@ contract WrappedMTokenMigratorV1 { assembly { sstore(_IMPLEMENTATION_SLOT, newImplementation_) } + + (bool success_, ) = address(this).call( + abi.encodeWithSelector( + bytes4(keccak256("initialize(address,address,address)")), + admin, + freezeManager, + pauser + ) + ); + + require(success_, "Initialize call failed"); } function _migrateEarners() internal { diff --git a/src/interfaces/ISwapFacilityLike.sol b/src/interfaces/ISwapFacilityLike.sol index 8d8042e..a876b45 100644 --- a/src/interfaces/ISwapFacilityLike.sol +++ b/src/interfaces/ISwapFacilityLike.sol @@ -7,6 +7,8 @@ pragma solidity 0.8.26; * @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. @@ -22,4 +24,13 @@ interface ISwapFacilityLike { * @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 0a2da10..a690926 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -88,6 +88,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @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 constructor if Excess Destination is 0x0. error ZeroExcessDestination(); diff --git a/test/utils/WrappedMTokenHarness.sol b/test/harness/WrappedMTokenHarness.sol similarity index 91% rename from test/utils/WrappedMTokenHarness.sol rename to test/harness/WrappedMTokenHarness.sol index feaf54e..5a0c1ed 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/harness/WrappedMTokenHarness.sol @@ -13,12 +13,12 @@ contract WrappedMTokenHarness is WrappedMToken { address migrationAdmin_ ) WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, migrationAdmin_) {} - function internalWrap(address recipient_, uint240 amount_) external { - _wrap(recipient_, amount_); + function internalWrap(address account_, address recipient_, uint240 amount_) external { + _wrap(account_, recipient_, amount_); } - function internalUnwrap(uint240 amount_) external { - _unwrap(amount_); + function internalUnwrap(address account_, uint240 amount_) external { + _unwrap(account_, amount_); } function setIsEarningOf(address account_, bool isEarning_) external { diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index e35cfc1..3e71f66 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -52,6 +52,9 @@ contract TestBase is Test { address internal _migrationAdmin = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal _m0Deployer = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _admin = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _freezeManager = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _pauser = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; address internal _alice = makeAddr("alice"); address internal _bob = makeAddr("bob"); @@ -198,7 +201,9 @@ contract TestBase is Test { earners_[index_] = _earners[index_]; } - _wrappedMTokenMigratorV1 = address(new WrappedMTokenMigratorV1(_wrappedMTokenImplementationV2, earners_)); + _wrappedMTokenMigratorV1 = address( + new WrappedMTokenMigratorV1(_wrappedMTokenImplementationV2, earners_, _admin, _freezeManager, _pauser) + ); } function _migrate() internal { diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 86a054c..90e7225 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -4,21 +4,35 @@ 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 { - address internal constant _WRAPPED_M_TOKEN = 0x437cc33344a0B27A429f795ff6B469C72698B291; + 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; + 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 _earners = [ 0x437cc33344a0B27A429f795ff6B469C72698B291, 0x0502d65f26f45d17503E4d34441F5e73Ea143033, @@ -82,7 +96,10 @@ contract UpgradeTests is Test, DeployBase { _EXCESS_DESTINATION, _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, - earners_ + earners_, + _ADMIN, + _FREEZE_MANAGER, + _PAUSER ); vm.stopPrank(); @@ -104,22 +121,32 @@ contract UpgradeTests is Test, DeployBase { balancesWithYield_[index_] = IWrappedMToken(_WRAPPED_M_TOKEN).balanceWithYieldOf(_earners[index_]); } - vm.prank(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin()); - IWrappedMToken(_WRAPPED_M_TOKEN).migrate(wrappedMTokenMigrator_); + vm.prank(_WRAPPED_M_TOKEN.migrationAdmin()); + _WRAPPED_M_TOKEN.migrate(wrappedMTokenMigrator_); // Wrapped M Token Proxy assertions - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).mToken(), _M_TOKEN); - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).registrar(), _REGISTRAR); - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).excessDestination(), _EXCESS_DESTINATION); - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).swapFacility(), _SWAP_FACILITY); - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).implementation(), wrappedMTokenImplementation_); + 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) + ); + + // Should not be able to call initialize again. + vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector)); + _WRAPPED_M_TOKEN.initialize(_ADMIN, _FREEZE_MANAGER, _PAUSER); // Relevant storage slots. - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(), totalEarningSupply_); + assertEq(_WRAPPED_M_TOKEN.totalEarningSupply(), totalEarningSupply_); for (uint256 index_; index_ < _earners.length; ++index_) { - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).balanceWithYieldOf(_earners[index_]), balancesWithYield_[index_]); + assertEq(_WRAPPED_M_TOKEN.balanceWithYieldOf(_earners[index_]), balancesWithYield_[index_]); } } } diff --git a/test/unit/Migrations.t.sol b/test/unit/Migrations.t.sol index c62ceb2..88f03f1 100644 --- a/test/unit/Migrations.t.sol +++ b/test/unit/Migrations.t.sol @@ -13,9 +13,19 @@ import { WrappedMTokenMigratorV1 as WrappedMTokenMigrator } from "../../src/Wrap import { MockRegistrar } from "./../utils/Mocks.sol"; contract Foo { + address public admin; + address public freezeManager; + address public pauser; + function bar() external pure returns (uint256) { return 1; } + + function initialize(address admin_, address freezeManager_, address pauser_) public { + admin = admin_; + freezeManager = freezeManager_; + pauser = pauser_; + } } contract MigrationTests is Test { @@ -28,9 +38,13 @@ contract MigrationTests is Test { address internal _dave = makeAddr("dave"); address internal _mToken = makeAddr("mToken"); - address internal _excessDestination = makeAddr("excessDestination"); address internal _swapFacility = makeAddr("swapFacility"); + + address internal _admin = makeAddr("admin"); + address internal _excessDestination = makeAddr("excessDestination"); + address internal _freezeManager = makeAddr("freezeManager"); address internal _migrationAdmin = makeAddr("migrationAdmin"); + address internal _pauser = makeAddr("pauser"); function test_wrappedMToken_migration() external { MockRegistrar registrar_ = new MockRegistrar(); @@ -41,7 +55,9 @@ contract MigrationTests is Test { ); address proxy_ = address(new Proxy(address(implementation_))); - address migrator_ = address(new WrappedMTokenMigrator(address(new Foo()), new address[](0))); + address migrator_ = address( + new WrappedMTokenMigrator(address(new Foo()), new address[](0), _admin, _freezeManager, _pauser) + ); registrar_.set(keccak256(abi.encode(_WM_MIGRATOR_KEY_PREFIX, proxy_)), bytes32(uint256(uint160(migrator_)))); @@ -51,6 +67,9 @@ contract MigrationTests is Test { IWrappedMToken(proxy_).migrate(); assertEq(Foo(proxy_).bar(), 1); + assertEq(Foo(proxy_).admin(), _admin); + assertEq(Foo(proxy_).freezeManager(), _freezeManager); + assertEq(Foo(proxy_).pauser(), _pauser); } function test_wrappedMToken_migration_fromAdmin() external { @@ -62,7 +81,9 @@ contract MigrationTests is Test { ); address proxy_ = address(new Proxy(address(implementation_))); - address migrator_ = address(new WrappedMTokenMigrator(address(new Foo()), new address[](0))); + address migrator_ = address( + new WrappedMTokenMigrator(address(new Foo()), new address[](0), _admin, _freezeManager, _pauser) + ); vm.expectRevert(); Foo(proxy_).bar(); @@ -71,5 +92,8 @@ contract MigrationTests is Test { IWrappedMToken(proxy_).migrate(migrator_); assertEq(Foo(proxy_).bar(), 1); + assertEq(Foo(proxy_).admin(), _admin); + assertEq(Foo(proxy_).freezeManager(), _freezeManager); + assertEq(Foo(proxy_).pauser(), _pauser); } } diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index ab16532..2729543 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -7,6 +7,7 @@ import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; import { Proxy } from "../../lib/common/src/Proxy.sol"; import { Test } from "../../lib/forge-std/src/Test.sol"; +import { ISwapFacilityLike } from "../../src/interfaces/ISwapFacilityLike.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; @@ -63,6 +64,12 @@ contract StoryTests is Test { _mToken.setBalanceOf(_alice, 100_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + vm.prank(_alice); _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _alice); @@ -79,6 +86,12 @@ contract StoryTests is Test { _mToken.setBalanceOf(_carol, 100_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_carol) + ); + vm.prank(_carol); _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _carol); @@ -113,6 +126,12 @@ contract StoryTests is Test { _mToken.setBalanceOf(_bob, 100_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_bob) + ); + vm.prank(_bob); _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _bob); @@ -130,6 +149,12 @@ contract StoryTests is Test { _mToken.setBalanceOf(_dave, 100_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_dave) + ); + vm.prank(_dave); _swapFacility.swapInM(address(_wrappedMToken), 100_000000, _dave); @@ -308,6 +333,12 @@ contract StoryTests is Test { vm.prank(_alice); _wrappedMToken.approve(address(_swapFacility), 266_666664); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_alice) + ); + vm.prank(_alice); _swapFacility.swapOutM(address(_wrappedMToken), 266_666664, _alice); @@ -325,6 +356,12 @@ contract StoryTests is Test { vm.prank(_bob); _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); @@ -342,6 +379,12 @@ contract StoryTests is Test { vm.prank(_carol); _wrappedMToken.approve(address(_swapFacility), 200_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_carol) + ); + vm.prank(_carol); _swapFacility.swapOutM(address(_wrappedMToken), 200_000000, _carol); @@ -359,6 +402,12 @@ contract StoryTests is Test { vm.prank(_dave); _wrappedMToken.approve(address(_swapFacility), 50_000000); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_dave) + ); + vm.prank(_dave); _swapFacility.swapOutM(address(_wrappedMToken), 50_000000, _dave); @@ -387,6 +436,12 @@ contract StoryTests 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); _swapFacility.swapInM(address(_wrappedMToken), 9, _alice); @@ -413,6 +468,12 @@ contract StoryTests is Test { vm.prank(_bob); _wrappedMToken.approve(address(_swapFacility), bobBalance_); + vm.mockCall( + address(_swapFacility), + abi.encodeWithSelector(ISwapFacilityLike.msgSender.selector), + abi.encode(_bob) + ); + vm.prank(_bob); _swapFacility.swapOutM(address(_wrappedMToken), bobBalance_, _bob); } @@ -430,6 +491,12 @@ contract StoryTests 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); _swapFacility.swapInM(address(_wrappedMToken), 1, _alice); diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 993bccc..f36d7d6 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -2,6 +2,14 @@ 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"; @@ -9,40 +17,22 @@ 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 { Test } from "../../lib/forge-std/src/Test.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"; -import { WrappedMTokenHarness } from "../utils/WrappedMTokenHarness.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 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 _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 _excessDestination = makeAddr("excessDestination"); - address internal _migrationAdmin = makeAddr("migrationAdmin"); - - address[] internal _accounts = [_alice, _bob, _charlie, _david]; - - MockM internal _mToken; - MockRegistrar internal _registrar; - MockSwapFacility internal _swapFacility; +contract WrappedMTokenTests is BaseUnitTest { WrappedMTokenHarness internal _implementation; - WrappedMTokenHarness internal _wrappedMToken; function setUp() external { _registrar = new MockRegistrar(); @@ -60,6 +50,7 @@ contract WrappedMTokenTests is Test { ); _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + _wrappedMToken.initialize(_admin, _freezeManager, _pauser); } /* ============ constants ============ */ @@ -121,12 +112,93 @@ contract WrappedMTokenTests is Test { 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)); + } + + function test_initialize_zeroAdmin() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IWrappedMToken.ZeroAdmin.selector); + wrappedMToken_.initialize(address(0), _freezeManager, _pauser); + } + + function test_initialize_zeroFreezeManager() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IFreezable.ZeroFreezeManager.selector); + wrappedMToken_.initialize(_admin, address(0), _pauser); + } + + function test_initialize_zeroPauser() external { + WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); + + vm.expectRevert(IPausable.ZeroPauser.selector); + wrappedMToken_.initialize(_admin, _freezeManager, address(0)); + } + + /* ============ _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, 0); + _wrappedMToken.internalWrap(_alice, _alice, 0); } function test_internalWrap_invalidRecipient() external { @@ -135,7 +207,7 @@ contract WrappedMTokenTests is Test { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); vm.prank(_alice); - _wrappedMToken.internalWrap(address(0), 1_000); + _wrappedMToken.internalWrap(_alice, address(0), 1_000); } function test_internalWrap_toNonEarner() external { @@ -157,7 +229,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(address(0), _alice, 1_000); vm.prank(_alice); - _wrappedMToken.internalWrap(_alice, 1_000); + _wrappedMToken.internalWrap(_alice, _alice, 1_000); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 2_000); @@ -168,7 +240,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 0); } - function test_wrap_toEarner() external { + function test_internalWrap_toEarner() external { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); @@ -191,7 +263,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(address(0), _alice, 999); vm.prank(_alice); - _wrappedMToken.internalWrap(_alice, 999); + _wrappedMToken.internalWrap(_alice, _alice, 999); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908); assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999); @@ -205,7 +277,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(address(0), _alice, 1); vm.prank(_alice); - _wrappedMToken.internalWrap(_alice, 1); + _wrappedMToken.internalWrap(_alice, _alice, 1); // No change due to principal round down on wrap. assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0); @@ -220,7 +292,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(address(0), _alice, 2); vm.prank(_alice); - _wrappedMToken.internalWrap(_alice, 2); + _wrappedMToken.internalWrap(_alice, _alice, 2); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 + 908 + 0 + 1); assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 999 + 1 + 2); @@ -239,8 +311,51 @@ contract WrappedMTokenTests is Test { _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); @@ -284,6 +399,12 @@ contract WrappedMTokenTests is Test { 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); @@ -303,6 +424,12 @@ contract WrappedMTokenTests is Test { 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); @@ -310,11 +437,31 @@ contract WrappedMTokenTests is Test { } /* ============ _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(0); + _wrappedMToken.internalUnwrap(_alice, 0); } function test_internalUnwrap_insufficientBalance_fromNonEarner() external { @@ -322,7 +469,7 @@ contract WrappedMTokenTests is Test { vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); vm.prank(_alice); - _wrappedMToken.internalUnwrap(1_000); + _wrappedMToken.internalUnwrap(_alice, 1_000); } function test_internalUnwrap_insufficientBalance_fromEarner() external { @@ -333,7 +480,7 @@ contract WrappedMTokenTests is Test { vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); vm.prank(_alice); - _wrappedMToken.internalUnwrap(1_000); + _wrappedMToken.internalUnwrap(_alice, 1_000); } function test_internalUnwrap_fromNonEarner() external { @@ -359,7 +506,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(_alice, address(0), 1); vm.prank(_alice); - _wrappedMToken.internalUnwrap(1); + _wrappedMToken.internalUnwrap(_alice, 1); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 999); @@ -373,7 +520,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(_alice, address(0), 499); vm.prank(_alice); - _wrappedMToken.internalUnwrap(499); + _wrappedMToken.internalUnwrap(_alice, 499); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 500); @@ -387,7 +534,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(_alice, address(0), 500); vm.prank(_alice); - _wrappedMToken.internalUnwrap(500); + _wrappedMToken.internalUnwrap(_alice, 500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 0); @@ -422,7 +569,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(_alice, address(0), 1); vm.prank(_alice); - _wrappedMToken.internalUnwrap(1); + _wrappedMToken.internalUnwrap(_alice, 1); // Change due to principal round up on unwrap. assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1); @@ -437,7 +584,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(_alice, address(0), 499); vm.prank(_alice); - _wrappedMToken.internalUnwrap(499); + _wrappedMToken.internalUnwrap(_alice, 499); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1 - 454); assertEq(_wrappedMToken.balanceOf(_alice), 1_000 - 1 - 499); @@ -451,7 +598,7 @@ contract WrappedMTokenTests is Test { emit IERC20.Transfer(_alice, address(0), 500); vm.prank(_alice); - _wrappedMToken.internalUnwrap(500); + _wrappedMToken.internalUnwrap(_alice, 500); assertEq(_wrappedMToken.earningPrincipalOf(_alice), 1_000 - 1 - 454 - 455); // 0 assertEq(_wrappedMToken.balanceOf(_alice), 1_000 - 1 - 499 - 500); // 0 @@ -470,6 +617,28 @@ contract WrappedMTokenTests is Test { _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); @@ -524,6 +693,12 @@ contract WrappedMTokenTests is Test { 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); @@ -538,6 +713,43 @@ contract WrappedMTokenTests is Test { } /* ============ 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); @@ -672,6 +884,14 @@ contract WrappedMTokenTests is Test { } /* ============ 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_, @@ -731,6 +951,16 @@ contract WrappedMTokenTests is Test { } /* ============ 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); @@ -740,6 +970,48 @@ contract WrappedMTokenTests is Test { _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); @@ -756,17 +1028,6 @@ contract WrappedMTokenTests is Test { _wrappedMToken.transfer(_bob, 1_000); } - function test_transfer_insufficientBalance_fromEarner_toNonEarner() external { - _mToken.setCurrentIndex(1_210000000000); - _wrappedMToken.setEnableMIndex(1_100000000000); - - _wrappedMToken.setAccountOf(_alice, 1_000, 1_000, false); // 1_100 balance with yield. - - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 1_000, 1_001)); - vm.prank(_alice); - _wrappedMToken.transfer(_bob, 1_001); - } - function test_transfer_fromNonEarner_toNonEarner() external { _wrappedMToken.setTotalNonEarningSupply(1_500); @@ -1044,6 +1305,16 @@ contract WrappedMTokenTests is Test { } /* ============ 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); @@ -1137,6 +1408,20 @@ contract WrappedMTokenTests is Test { } /* ============ 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)); @@ -1184,6 +1469,48 @@ contract WrappedMTokenTests is Test { _wrappedMToken.stopEarningFor(_alice); } + function test_stopEarningFor_enforcedPause() external { + _wrappedMToken.setIsEarningOf(_alice, true); + + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + + _wrappedMToken.stopEarningFor(_alice); + } + + 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 { + _wrappedMToken.setIsEarningOf(_alice, true); + + _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)); + + _wrappedMToken.stopEarningFor(_alice); + } + function test_stopEarningFor() external { _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); diff --git a/test/utils/BaseUnitTest.sol b/test/utils/BaseUnitTest.sol new file mode 100644 index 0000000..aa8de17 --- /dev/null +++ b/test/utils/BaseUnitTest.sol @@ -0,0 +1,41 @@ +// 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 _PAUSER_ROLE = keccak256("PAUSER_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 _excessDestination = makeAddr("excessDestination"); + address internal _freezeManager = makeAddr("freezeManager"); + 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/Mocks.sol b/test/utils/Mocks.sol index afe6242..db539b7 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -67,7 +67,7 @@ contract MockM { isEarning[msg.sender] = false; } - function approve(address spender_, uint256 amount_) external returns (bool success_) { + function approve(address /* spender_ */, uint256 /* amount_ */) external pure returns (bool success_) { return true; } } From 9a9f0ea991d7d8be925ad9ad94f71abbc83d0b9e Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Tue, 16 Dec 2025 04:54:22 -0600 Subject: [PATCH 18/27] feat(WrappedM): mint WM to excess destination instead of M (#126) --- foundry.toml | 2 +- src/WrappedMToken.sol | 3 +-- test/integration/Protocol.t.sol | 14 +++++--------- test/unit/Stories.t.sol | 4 +++- test/unit/WrappedMToken.t.sol | 2 ++ 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/foundry.toml b/foundry.toml index a333706..f6fec6c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ gas_reports_ignore = [] ignored_error_codes = [] solc_version = "0.8.26" optimizer = true -optimizer_runs = 22099 +optimizer_runs = 21883 verbosity = 3 fork_block_number = 23_170_985 rpc_storage_caching = { chains = ["mainnet"], endpoints = "all" } diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 1e95533..cee486e 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -185,8 +185,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, emit ExcessClaimed(claimed_ = uint240(uint256(excess_))); - // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. - IMTokenLike(mToken).transfer(excessDestination, claimed_); + _mint(excessDestination, claimed_); } /// @inheritdoc IWrappedMToken diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index 467f295..e725ee8 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -620,22 +620,18 @@ contract ProtocolIntegrationTests is TestBase { int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess ); - uint256 vaultStartingBalance_ = _mToken.balanceOf(_excessDestination); + uint256 vaultStartingBalance_ = _wrappedMToken.balanceOf(_excessDestination); assertEq(_wrappedMToken.claimExcess(), uint256(_excess)); - - assertEq(_mToken.balanceOf(_excessDestination), uint256(_excess) + vaultStartingBalance_); + assertEq(_wrappedMToken.balanceOf(_excessDestination), uint256(_excess) + vaultStartingBalance_); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); - assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); + assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += uint256(_excess)); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= (_excess + 1)); // Rounding error + assertEq(_wrappedMToken.excess(), 0); - assertGe( - int256(_wrapperBalanceOfM), - int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield) + _excess - ); + assertGe(int256(_wrapperBalanceOfM), int256(_totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield)); } function testFuzz_full(uint256 seed_) external { diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index 2729543..ef998f2 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -459,7 +459,9 @@ contract StoryTests is Test { _wrappedMToken.transfer(_bob, aliceBalance_); assertLe( - int256(_wrappedMToken.balanceOf(_bob)) + int256(_wrappedMToken.excess()), + int256(_wrappedMToken.balanceOf(_bob)) + + int256(_wrappedMToken.excess()) + + int256(_wrappedMToken.balanceOf(_excessDestination)), int256(_mToken.balanceOf(address(_wrappedMToken))) ); diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index f36d7d6..9053894 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -948,6 +948,8 @@ contract WrappedMTokenTests is BaseUnitTest { } else { assertLe(claimed_, uint240(excess_)); } + + assertEq(_wrappedMToken.balanceOf(_excessDestination), claimed_); } /* ============ transfer ============ */ From 81d97a147db324a5bd3b553ad08c3505bbc60dfc Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 13 Feb 2026 10:58:41 -0600 Subject: [PATCH 19/27] chore(script): add script to retrieve WM earners (#127) --- README.md | 1 - earners/arbitrum.csv | 4 + earners/ethereum.csv | 25 + earners/get-earners.ts | 130 ++ earners/plume.csv | 2 + package-lock.json | 2200 +++++++++++++++++++++++++++++ package.json | 1 + script/DeployUpgradeMainnet.s.sol | 57 +- script/EarnersAddresses.sol | 59 + script/generate-earners-array.ts | 215 +++ test/integration/Upgrade.t.sol | 47 +- yarn.lock | 1113 --------------- 12 files changed, 2668 insertions(+), 1186 deletions(-) create mode 100644 earners/arbitrum.csv create mode 100644 earners/ethereum.csv create mode 100755 earners/get-earners.ts create mode 100644 earners/plume.csv create mode 100644 package-lock.json create mode 100644 script/EarnersAddresses.sol create mode 100755 script/generate-earners-array.ts delete mode 100644 yarn.lock 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/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/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 e5826db..3cb2bc1 100644 --- a/package.json +++ b/package.json @@ -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/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol index f3d19b7..9d0fcb9 100644 --- a/script/DeployUpgradeMainnet.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -5,6 +5,7 @@ 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); @@ -52,44 +53,18 @@ contract DeployUpgradeMainnet is Script, DeployBase { // NOTE: Ensure this is the correct expected mainnet address for the Migrator. address internal constant _EXPECTED_WRAPPED_M_MIGRATOR = address(0); - // NOTE: Ensure this is the correct and complete list of earners on mainnet. - 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 - ]; + // 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")); @@ -126,11 +101,7 @@ contract DeployUpgradeMainnet is Script, DeployBase { if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); - address[] memory earners_ = new address[](_earners.length); - - for (uint256 index_; index_ < _earners.length; ++index_) { - earners_[index_] = _earners[index_]; - } + address[] memory earners_ = getEarners(); (wrappedMTokenImplementation_, wrappedMTokenMigrator_) = deployUpgrade( _M_TOKEN, diff --git a/script/EarnersAddresses.sol b/script/EarnersAddresses.sol new file mode 100644 index 0000000..a138dcf --- /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 = [ + 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, + 0xfF95c5f35F4ffB9d5f596F898ac1ae38D62749c2, + 0x0502d65f26f45d17503E4d34441F5e73Ea143033, + 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, + 0x81ad394C0Fa87e99Ca46E1aca093BEe020f203f4, + 0xfE940BFE535013a52e8e2DF9644f95E3C94fa14B, + 0xE0663f2372cAa1459b7ade90812Dc737CE587FA6, + 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, + 0x9c6e67fA86138Ab49359F595BfE4Fb163D0f16cc, + 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, + 0xB65a66621D7dE34afec9b9AC0755133051550dD7, + 0xcAD001c30E96765aC90307669d578219D4fb1DCe, + 0xDeD796De6a14E255487191963dEe436c45995813, + 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90, + 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, + 0x13Ccb6E28F22E2f6783BaDedCe32cc74583A3647, + 0x985DE23260743c2c2f09BFdeC50b048C7a18c461, + 0x7db685961F97c847A4C815D43E9cc0E5647328b9, + 0x48Afe17cB6363fD1aaeA50a8CB652C5978972c96, + 0xE72Fe64840F4EF80E3Ec73a1c749491b5c938CB9, + 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, + 0xB50A1f651A5ACb2679c8f679D782c728f3702E53, + 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70, + 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec + ]; + 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..4e0ac46 --- /dev/null +++ b/script/generate-earners-array.ts @@ -0,0 +1,215 @@ +#!/usr/bin/env tsx + +import { readFileSync, writeFileSync } from "fs"; +import { join } from "path"; +import { getAddress } from "ethers"; + +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 generateFunctionName(network: string): string { + return `get${network.charAt(0).toUpperCase() + network.slice(1)}Earners`; +} + +function generateSolidityFunction( + network: string, + earners: WMHolder[], +): string { + const functionName = generateFunctionName(network); + const earnerAddresses = earners.map((e) => getAddress(e.address)); + const arrayLength = earnerAddresses.length; + + const addressesLines = earnerAddresses + .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 getOrCreateSolidityFile(): string { + const outputPath = join("script", "EarnersAddresses.sol"); + + try { + return readFileSync(outputPath, "utf-8"); + } catch (error) { + return `// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.26; + +library EarnersAddresses { +} +`; + } +} + +function updateSolidityFile(network: string, earners: WMHolder[]): void { + const outputPath = join("script", "EarnersAddresses.sol"); + let content = getOrCreateSolidityFile(); + const functionName = generateFunctionName(network); + + const newFunction = generateSolidityFunction(network, earners); + + if (content.includes(functionName)) { + const functionStart = content.indexOf(`function ${functionName}`); + if (functionStart === -1) { + throw new Error(`Could not find function ${functionName}`); + } + + const openBrace = content.indexOf("{", functionStart); + if (openBrace === -1) { + throw new Error(`Could not find opening brace for ${functionName}`); + } + + let braceCount = 0; + let functionEnd = -1; + for (let i = openBrace; i < content.length; i++) { + if (content[i] === "{") braceCount++; + else if (content[i] === "}") { + braceCount--; + if (braceCount === 0) { + functionEnd = i; + break; + } + } + } + + if (functionEnd === -1) { + throw new Error(`Could not find closing brace for ${functionName}`); + } + + content = + content.slice(0, functionStart) + + newFunction + + content.slice(functionEnd + 1); + console.log(`Updated existing ${functionName}() function`); + } else { + const libraryEndIndex = content.lastIndexOf("}"); + if (libraryEndIndex === -1) { + throw new Error("Invalid Solidity file format"); + } + content = + content.slice(0, libraryEndIndex) + + newFunction + + content.slice(libraryEndIndex); + console.log(`Added new ${functionName}() function`); + } + + writeFileSync(outputPath, content); +} + +async function main() { + const networks = [ + "arbitrum", + "base", + "bsc", + "ethereum", + "hyperevm", + "linea", + "mantra", + "optimism", + "plasma", + "plume", + "soneium", + ]; + + 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; + } + + updateSolidityFile(network, earners); + + const functionName = generateFunctionName(network); + console.log( + `Added ${functionName}() with ${earners.length} checksummed addresses to EarnersAddresses.sol`, + ); + } catch (error) { + console.error( + `Error processing ${networkName}:`, + error instanceof Error ? error.message : error, + ); + } + } +} + +main(); diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 90e7225..197f19f 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -34,41 +34,30 @@ contract UpgradeTests is Test, DeployBase { address internal constant _PAUSER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; address[] internal _earners = [ - 0x437cc33344a0B27A429f795ff6B469C72698B291, + 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, + 0xfF95c5f35F4ffB9d5f596F898ac1ae38D62749c2, 0x0502d65f26f45d17503E4d34441F5e73Ea143033, - 0x061110360ba50E19139a1Bf2EaF4004FB0dD31e8, - 0x9106CBf2C882340b23cC40985c05648173E359e7, - 0x846E7F810E08F1E2AF2c5AfD06847cc95F5CaE1B, - 0x967B10c27454CC5b1b1Eeb163034ACdE13Fe55e2, - 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, - 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90, - 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, - 0x184d597Be309e11650ca6c935B483DcC05551578, - 0xA259E266a43F3070CecD80F05C8947aB93c074Ba, - 0x0f71a8e95A918A4A984Ad3841414cD00D9C13e7d, - 0xf3CfA6e51b2B580AE6Ad71e2D719Ab09e4A0D7aa, - 0x56721131d21a170fBb084734DcC399A278234298, - 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, - 0xDeD796De6a14E255487191963dEe436c45995813, - 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, - 0x8F9139Fe15E561De5fCe39DB30856924Dd67Af0e, 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, + 0x81ad394C0Fa87e99Ca46E1aca093BEe020f203f4, + 0xfE940BFE535013a52e8e2DF9644f95E3C94fa14B, + 0xE0663f2372cAa1459b7ade90812Dc737CE587FA6, + 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, + 0x9c6e67fA86138Ab49359F595BfE4Fb163D0f16cc, + 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, 0xB65a66621D7dE34afec9b9AC0755133051550dD7, 0xcAD001c30E96765aC90307669d578219D4fb1DCe, - 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70, - 0x9c6e67fA86138Ab49359F595BfE4Fb163D0f16cc, - 0xABFD9948933b975Ee9a668a57C776eCf73F6D840, - 0x7FDA203f6F77545548E984133be62693bCD61497, - 0xa8687A15D4BE32CC8F0a8a7B9704a4C3993D9613, - 0x3f0376da3Ae4313E7a5F1dA184BAFC716252d759, - 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec, - 0xcEa14C3e9Afc5822d44ADe8d006fCFBAb60f7a21, - 0x81ad394C0Fa87e99Ca46E1aca093BEe020f203f4, - 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, + 0xDeD796De6a14E255487191963dEe436c45995813, + 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90, + 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, 0x13Ccb6E28F22E2f6783BaDedCe32cc74583A3647, 0x985DE23260743c2c2f09BFdeC50b048C7a18c461, - 0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd, - 0x20b3a4119eAB75ffA534aC8fC5e9160BdcaF442b + 0x7db685961F97c847A4C815D43E9cc0E5647328b9, + 0x48Afe17cB6363fD1aaeA50a8CB652C5978972c96, + 0xE72Fe64840F4EF80E3Ec73a1c749491b5c938CB9, + 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, + 0xB50A1f651A5ACb2679c8f679D782c728f3702E53, + 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70, + 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec ]; function setUp() public { 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== From 5517ff8f15e8d91fb920530719f9e1b25c447edc Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Sun, 31 May 2026 17:45:36 -0500 Subject: [PATCH 20/27] feat(WrappedM): add forceTransfer functionality with FORCED_TRANSFER_MANAGER_ROLE (#128) --- .gitmodules | 2 +- foundry.lock | 8 +- lib/common | 2 +- lib/evm-m-extensions | 2 +- lib/forge-std | 2 +- script/DeployBase.sol | 12 +- script/DeployUpgradeMainnet.s.sol | 6 +- src/WrappedMToken.sol | 85 ++++++-- src/WrappedMTokenMigratorV1.sol | 10 +- test/integration/TestBase.sol | 10 +- test/integration/Upgrade.t.sol | 12 +- test/unit/Migrations.t.sol | 25 ++- test/unit/WrappedMToken.t.sol | 330 +++++++++++++++++++++++++++++- test/utils/BaseUnitTest.sol | 2 + 14 files changed, 465 insertions(+), 43 deletions(-) diff --git a/.gitmodules b/.gitmodules index cfad28d..01078a7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,7 +5,7 @@ [submodule "lib/common"] path = lib/common url = https://github.com/m0-foundation/common - branch = release-v1.4.0 + branch = release-v1.5.1 [submodule "lib/evm-m-extensions"] path = lib/evm-m-extensions url = https://github.com/m0-foundation/evm-m-extensions diff --git a/foundry.lock b/foundry.lock index 317c342..cc08885 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,17 +1,17 @@ { "lib/common": { "branch": { - "name": "release-v1.4.0", - "rev": "613d2d95a476612270324ecbe92317eb5e9bc98f" + "name": "release-v1.5.1", + "rev": "20440d03de21fe62970829245872e9adae6cd822" } }, "lib/evm-m-extensions": { - "rev": "f85daca886802be8e170f84dbb45cede6f460a20" + "rev": "87a2f42ab55b5e10771f48fa0d5994bfaa3dca09" }, "lib/forge-std": { "branch": { "name": "v1", - "rev": "7117c90c8cf6c68e5acce4f09a6b24715cea4de6" + "rev": "620536fa5277db4e3fd46772d5cbc1ea0696fb43" } } } \ No newline at end of file diff --git a/lib/common b/lib/common index 613d2d9..20440d0 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 613d2d95a476612270324ecbe92317eb5e9bc98f +Subproject commit 20440d03de21fe62970829245872e9adae6cd822 diff --git a/lib/evm-m-extensions b/lib/evm-m-extensions index f85daca..87a2f42 160000 --- a/lib/evm-m-extensions +++ b/lib/evm-m-extensions @@ -1 +1 @@ -Subproject commit f85daca886802be8e170f84dbb45cede6f460a20 +Subproject commit 87a2f42ab55b5e10771f48fa0d5994bfaa3dca09 diff --git a/lib/forge-std b/lib/forge-std index 7117c90..620536f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 7117c90c8cf6c68e5acce4f09a6b24715cea4de6 +Subproject commit 620536fa5277db4e3fd46772d5cbc1ea0696fb43 diff --git a/script/DeployBase.sol b/script/DeployBase.sol index 56512f4..07efa5c 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -56,14 +56,22 @@ contract DeployBase { address[] memory earners_, address admin_, address freezeManager_, - address pauser_ + address pauser_, + address forcedTransferManager_ ) public virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) { wrappedMTokenImplementation_ = address( new WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) ); wrappedMTokenMigrator_ = address( - new WrappedMTokenMigratorV1(wrappedMTokenImplementation_, earners_, admin_, freezeManager_, pauser_) + new WrappedMTokenMigratorV1( + wrappedMTokenImplementation_, + earners_, + admin_, + freezeManager_, + pauser_, + forcedTransferManager_ + ) ); } diff --git a/script/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol index 9d0fcb9..6f89b72 100644 --- a/script/DeployUpgradeMainnet.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -44,6 +44,9 @@ contract DeployUpgradeMainnet is Script, DeployBase { // 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 mainnet deployer to use. address internal constant _EXPECTED_DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; @@ -112,7 +115,8 @@ contract DeployUpgradeMainnet is Script, DeployBase { earners_, _ADMIN, _FREEZE_MANAGER, - _PAUSER + _PAUSER, + _FORCED_TRANSFER_MANAGER ); vm.stopBroadcast(); diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index cee486e..5a93925 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -5,6 +5,7 @@ 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"; @@ -33,7 +34,7 @@ import { IWrappedMToken } from "./interfaces/IWrappedMToken.sol"; * @title ERC20 Token contract for wrapping M into a non-rebasing token with claimable yields. * @author M0 Labs */ -contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, Pausable { +contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, Pausable, ForcedTransferable { /* ============ Structs ============ */ /** @@ -141,16 +142,23 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /** * @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 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. */ - function initialize(address admin_, address freezeManager_, address pauser_) public initializer { + function initialize( + address admin_, + address freezeManager_, + address pauser_, + address forcedTransferManager_ + ) public initializer { if (admin_ == address(0)) revert ZeroAdmin(); _grantRole(DEFAULT_ADMIN_ROLE, admin_); __Freezable_init(freezeManager_); __Pausable_init(pauser_); + __ForcedTransferable_init(forcedTransferManager_); } /* ============ Interactive Functions ============ */ @@ -172,7 +180,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /// @inheritdoc IWrappedMToken function claimFor(address account_) external returns (uint240 yield_) { - return _claim(account_, currentIndex()); + _requireNotPaused(); + _revertIfFrozen(account_); + + return _claim(account_, currentIndex(), false); } /// @inheritdoc IWrappedMToken @@ -230,15 +241,22 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /// @inheritdoc IWrappedMToken function stopEarningFor(address account_) external { + _revertIfFrozen(account_); + _revertIfApprovedEarner(account_); + _stopEarningFor(account_, currentIndex()); } /// @inheritdoc IWrappedMToken function stopEarningFor(address[] calldata accounts_) external { uint128 currentIndex_ = currentIndex(); + FreezableStorageStruct storage $ = _getFreezableStorageLocation(); - for (uint256 index_; index_ < accounts_.length; ++index_) { - _stopEarningFor(accounts_[index_], currentIndex_); + for (uint256 i; i < accounts_.length; ++i) { + _revertIfFrozen($, accounts_[i]); + _revertIfApprovedEarner(accounts_[i]); + + _stopEarningFor(accounts_[i], currentIndex_); } } @@ -488,12 +506,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, * @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_) { - _requireNotPaused(); - _revertIfFrozen(account_); - + function _claim(address account_, uint128 currentIndex_, bool skipTransfer_) internal returns (uint240 yield_) { Account storage accountInfo_ = _accounts[account_]; if (!accountInfo_.isEarning) return 0; @@ -516,7 +532,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, emit Claimed(account_, claimRecipient_, yield_); emit Transfer(address(0), account_, yield_); - if ((claimRecipient_ == account_) || (yield_ == 0)) return yield_; + if (skipTransfer_ || claimRecipient_ == account_) return yield_; _transfer(account_, claimRecipient_, yield_, currentIndex_); } @@ -732,18 +748,16 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, } /** - * @dev Stops earning for `account` if disallowed by the Registrar. + * @dev Stops earning for `account` given some current index. * @param account_ The account to stop earning for. * @param currentIndex_ The current index. */ function _stopEarningFor(address account_, uint128 currentIndex_) internal { - _revertIfApprovedEarner(account_); - Account storage accountInfo_ = _accounts[account_]; if (!accountInfo_.isEarning) return; - _claim(account_, currentIndex_); + _claim(account_, currentIndex_, paused()); uint240 balance_ = accountInfo_.balance; uint112 earningPrincipal_ = accountInfo_.earningPrincipal; @@ -760,6 +774,43 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, emit StoppedEarning(account_); } + /** + * @dev Hook called before freezing an account. Claims accrued yield and stops earning. + * @param account_ The account about to be frozen. + */ + function _beforeFreeze(address account_) internal override { + _stopEarningFor(account_, currentIndex()); + + 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 ============ */ /// @dev Returns the current index of the M Token. diff --git a/src/WrappedMTokenMigratorV1.sol b/src/WrappedMTokenMigratorV1.sol index 62e2157..d13ff70 100644 --- a/src/WrappedMTokenMigratorV1.sol +++ b/src/WrappedMTokenMigratorV1.sol @@ -54,13 +54,15 @@ contract WrappedMTokenMigratorV1 { address public immutable admin; address public immutable freezeManager; address public immutable pauser; + address public immutable forcedTransferManager; constructor( address newImplementation_, address[] memory earners_, address admin_, address freezeManager_, - address pauser_ + address pauser_, + address forcedTransferManager_ ) { newImplementation = newImplementation_; @@ -69,6 +71,7 @@ contract WrappedMTokenMigratorV1 { admin = admin_; freezeManager = freezeManager_; pauser = pauser_; + forcedTransferManager = forcedTransferManager_; } fallback() external virtual { @@ -90,10 +93,11 @@ contract WrappedMTokenMigratorV1 { (bool success_, ) = address(this).call( abi.encodeWithSelector( - bytes4(keccak256("initialize(address,address,address)")), + bytes4(keccak256("initialize(address,address,address,address)")), admin, freezeManager, - pauser + pauser, + forcedTransferManager ) ); diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 3e71f66..0d427cb 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -55,6 +55,7 @@ contract TestBase is Test { address internal _admin = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; address internal _freezeManager = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; address internal _pauser = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + address internal _forcedTransferManager = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; address internal _alice = makeAddr("alice"); address internal _bob = makeAddr("bob"); @@ -202,7 +203,14 @@ contract TestBase is Test { } _wrappedMTokenMigratorV1 = address( - new WrappedMTokenMigratorV1(_wrappedMTokenImplementationV2, earners_, _admin, _freezeManager, _pauser) + new WrappedMTokenMigratorV1( + _wrappedMTokenImplementationV2, + earners_, + _admin, + _freezeManager, + _pauser, + _forcedTransferManager + ) ); } diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 197f19f..990f4cb 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -32,6 +32,7 @@ contract UpgradeTests is Test, DeployBase { 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 _earners = [ 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, @@ -88,7 +89,8 @@ contract UpgradeTests is Test, DeployBase { earners_, _ADMIN, _FREEZE_MANAGER, - _PAUSER + _PAUSER, + _FORCED_TRANSFER_MANAGER ); vm.stopPrank(); @@ -126,10 +128,16 @@ contract UpgradeTests is Test, DeployBase { 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 + ) + ); // Should not be able to call initialize again. vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector)); - _WRAPPED_M_TOKEN.initialize(_ADMIN, _FREEZE_MANAGER, _PAUSER); + _WRAPPED_M_TOKEN.initialize(_ADMIN, _FREEZE_MANAGER, _PAUSER, _FORCED_TRANSFER_MANAGER); // Relevant storage slots. assertEq(_WRAPPED_M_TOKEN.totalEarningSupply(), totalEarningSupply_); diff --git a/test/unit/Migrations.t.sol b/test/unit/Migrations.t.sol index 88f03f1..51bdaa0 100644 --- a/test/unit/Migrations.t.sol +++ b/test/unit/Migrations.t.sol @@ -16,15 +16,17 @@ contract Foo { address public admin; address public freezeManager; address public pauser; + address public forcedTransferManager; function bar() external pure returns (uint256) { return 1; } - function initialize(address admin_, address freezeManager_, address pauser_) public { + function initialize(address admin_, address freezeManager_, address pauser_, address forcedTransferManager_) public { admin = admin_; freezeManager = freezeManager_; pauser = pauser_; + forcedTransferManager = forcedTransferManager_; } } @@ -43,6 +45,7 @@ contract MigrationTests is Test { address internal _admin = makeAddr("admin"); 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"); @@ -56,7 +59,14 @@ contract MigrationTests is Test { address proxy_ = address(new Proxy(address(implementation_))); address migrator_ = address( - new WrappedMTokenMigrator(address(new Foo()), new address[](0), _admin, _freezeManager, _pauser) + new WrappedMTokenMigrator( + address(new Foo()), + new address[](0), + _admin, + _freezeManager, + _pauser, + _forcedTransferManager + ) ); registrar_.set(keccak256(abi.encode(_WM_MIGRATOR_KEY_PREFIX, proxy_)), bytes32(uint256(uint160(migrator_)))); @@ -70,6 +80,7 @@ contract MigrationTests is Test { assertEq(Foo(proxy_).admin(), _admin); assertEq(Foo(proxy_).freezeManager(), _freezeManager); assertEq(Foo(proxy_).pauser(), _pauser); + assertEq(Foo(proxy_).forcedTransferManager(), _forcedTransferManager); } function test_wrappedMToken_migration_fromAdmin() external { @@ -82,7 +93,14 @@ contract MigrationTests is Test { address proxy_ = address(new Proxy(address(implementation_))); address migrator_ = address( - new WrappedMTokenMigrator(address(new Foo()), new address[](0), _admin, _freezeManager, _pauser) + new WrappedMTokenMigrator( + address(new Foo()), + new address[](0), + _admin, + _freezeManager, + _pauser, + _forcedTransferManager + ) ); vm.expectRevert(); @@ -95,5 +113,6 @@ contract MigrationTests is Test { assertEq(Foo(proxy_).admin(), _admin); assertEq(Foo(proxy_).freezeManager(), _freezeManager); assertEq(Foo(proxy_).pauser(), _pauser); + assertEq(Foo(proxy_).forcedTransferManager(), _forcedTransferManager); } } diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 9053894..dd89f5e 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -18,6 +18,9 @@ import { IERC20Extended } from "../../lib/common/src/interfaces/IERC20Extended.s 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"; @@ -50,7 +53,7 @@ contract WrappedMTokenTests is BaseUnitTest { ); _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); - _wrappedMToken.initialize(_admin, _freezeManager, _pauser); + _wrappedMToken.initialize(_admin, _freezeManager, _pauser, _forcedTransferManager); } /* ============ constants ============ */ @@ -118,27 +121,37 @@ contract WrappedMTokenTests is BaseUnitTest { 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) + ); } function test_initialize_zeroAdmin() external { WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); vm.expectRevert(IWrappedMToken.ZeroAdmin.selector); - wrappedMToken_.initialize(address(0), _freezeManager, _pauser); + wrappedMToken_.initialize(address(0), _freezeManager, _pauser, _forcedTransferManager); } function test_initialize_zeroFreezeManager() external { WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); vm.expectRevert(IFreezable.ZeroFreezeManager.selector); - wrappedMToken_.initialize(_admin, address(0), _pauser); + wrappedMToken_.initialize(_admin, address(0), _pauser, _forcedTransferManager); } function test_initialize_zeroPauser() external { WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); vm.expectRevert(IPausable.ZeroPauser.selector); - wrappedMToken_.initialize(_admin, _freezeManager, address(0)); + wrappedMToken_.initialize(_admin, _freezeManager, address(0), _forcedTransferManager); + } + + 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)); } /* ============ _approve ============ */ @@ -1472,14 +1485,31 @@ contract WrappedMTokenTests is BaseUnitTest { } function test_stopEarningFor_enforcedPause() external { - _wrappedMToken.setIsEarningOf(_alice, true); + _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(); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + // 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_frozenAccount() external { @@ -1508,6 +1538,7 @@ contract WrappedMTokenTests is BaseUnitTest { vm.prank(_freezeManager); _wrappedMToken.freeze(_bob); + // Not paused, so skipTransfer=false: _claim tries to transfer yield to frozen _bob → reverts. vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _bob)); _wrappedMToken.stopEarningFor(_alice); @@ -1657,6 +1688,293 @@ contract WrappedMTokenTests is BaseUnitTest { _wrappedMToken.stopEarningFor(accounts_); } + /* ============ 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 { + _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); + + // Pause the contract so skipTransfer=true in _beforeFreeze -> _stopEarningFor -> _claim. + vm.prank(_pauser); + _wrappedMToken.pause(); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _bob, 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.balanceOf(_bob), 0); // yield not routed (paused) + 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_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))); diff --git a/test/utils/BaseUnitTest.sol b/test/utils/BaseUnitTest.sol index aa8de17..768df6c 100644 --- a/test/utils/BaseUnitTest.sol +++ b/test/utils/BaseUnitTest.sol @@ -17,6 +17,7 @@ contract BaseUnitTest is Test { 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 _EARNERS_LIST_NAME = "earners"; @@ -29,6 +30,7 @@ contract BaseUnitTest is Test { address internal _admin = makeAddr("admin"); 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"); From 699860f600298a1c90e4e3f8133477c952d30bcd Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 5 Jun 2026 12:23:03 -0500 Subject: [PATCH 21/27] fix(WrappedM): fix frozen check inconsistencies (#130) --- src/WrappedMToken.sol | 18 +++++--- test/unit/WrappedMToken.t.sol | 80 ++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 5a93925..4191331 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -244,19 +244,20 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, _revertIfFrozen(account_); _revertIfApprovedEarner(account_); - _stopEarningFor(account_, currentIndex()); + _stopEarningFor(account_, currentIndex(), paused()); } /// @inheritdoc IWrappedMToken function stopEarningFor(address[] calldata accounts_) external { uint128 currentIndex_ = currentIndex(); + bool skipTransfer_ = paused(); FreezableStorageStruct storage $ = _getFreezableStorageLocation(); for (uint256 i; i < accounts_.length; ++i) { _revertIfFrozen($, accounts_[i]); _revertIfApprovedEarner(accounts_[i]); - _stopEarningFor(accounts_[i], currentIndex_); + _stopEarningFor(accounts_[i], currentIndex_, skipTransfer_); } } @@ -702,6 +703,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, */ function _unwrap(address account_, uint240 amount_) internal { _requireNotPaused(); + + // 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. @@ -722,6 +726,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, */ function _startEarningFor(address account_, uint128 currentIndex_) internal { _requireNotPaused(); + _revertIfFrozen(account_); _revertIfNotApprovedEarner(account_); Account storage accountInfo_ = _accounts[account_]; @@ -751,13 +756,14 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, * @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_) internal { + function _stopEarningFor(address account_, uint128 currentIndex_, bool skipTransfer_) internal { Account storage accountInfo_ = _accounts[account_]; if (!accountInfo_.isEarning) return; - _claim(account_, currentIndex_, paused()); + _claim(account_, currentIndex_, skipTransfer_); uint240 balance_ = accountInfo_.balance; uint112 earningPrincipal_ = accountInfo_.earningPrincipal; @@ -776,10 +782,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /** * @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()); + _stopEarningFor(account_, currentIndex(), true); super._beforeFreeze(account_); } diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index dd89f5e..5ffa2be 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -1343,6 +1343,19 @@ contract WrappedMTokenTests is BaseUnitTest { _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); @@ -1456,6 +1469,24 @@ contract WrappedMTokenTests is BaseUnitTest { _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); @@ -1725,6 +1756,10 @@ contract WrappedMTokenTests is BaseUnitTest { } 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); @@ -1739,10 +1774,6 @@ contract WrappedMTokenTests is BaseUnitTest { vm.prank(_freezeManager); _wrappedMToken.freeze(_bob); - // Pause the contract so skipTransfer=true in _beforeFreeze -> _stopEarningFor -> _claim. - vm.prank(_pauser); - _wrappedMToken.pause(); - vm.expectEmit(); emit IWrappedMToken.Claimed(_alice, _bob, 100); @@ -1758,8 +1789,8 @@ contract WrappedMTokenTests is BaseUnitTest { vm.prank(_freezeManager); _wrappedMToken.freeze(_alice); - assertEq(_wrappedMToken.balanceOf(_alice), 1_100); - assertEq(_wrappedMToken.balanceOf(_bob), 0); // yield not routed (paused) + 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)); } @@ -1834,6 +1865,43 @@ contract WrappedMTokenTests is BaseUnitTest { _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); From c6327a2ab642eae626adb88af1b4d5590068acd3 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 5 Jun 2026 12:26:58 -0500 Subject: [PATCH 22/27] feat(WrappedM): add EXCESS_MANAGER_ROLE and settable excessDestination (#131) --- foundry.toml | 2 +- script/Deploy.s.sol | 1 - script/DeployBase.sol | 64 ++++++--- script/DeployUpgradeMainnet.s.sol | 17 ++- src/WrappedMToken.sol | 54 +++++--- src/WrappedMTokenMigratorV1.sol | 14 +- src/interfaces/IEarnerManager.sol | 178 -------------------------- src/interfaces/IWrappedMToken.sol | 21 ++- test/harness/WrappedMTokenHarness.sol | 3 +- test/integration/Deploy.t.sol | 3 - test/integration/TestBase.sol | 7 +- test/integration/Upgrade.t.sol | 25 +++- test/unit/Migrations.t.sol | 30 ++++- test/unit/Stories.t.sol | 14 +- test/unit/WrappedMToken.t.sol | 127 +++++++++++++++--- test/utils/BaseUnitTest.sol | 2 + 16 files changed, 295 insertions(+), 267 deletions(-) delete mode 100644 src/interfaces/IEarnerManager.sol diff --git a/foundry.toml b/foundry.toml index f6fec6c..5ea4429 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ gas_reports_ignore = [] ignored_error_codes = [] solc_version = "0.8.26" optimizer = true -optimizer_runs = 21883 +optimizer_runs = 4150 verbosity = 3 fork_block_number = 23_170_985 rpc_storage_caching = { chains = ["mainnet"], endpoints = "all" } diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 7e18fb0..9f36445 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -62,7 +62,6 @@ contract DeployProduction is Script, DeployBase { (wrappedMTokenImplementation_, wrappedMTokenProxy_) = deploy( vm.envAddress("M_TOKEN"), vm.envAddress("REGISTRAR"), - vm.envAddress("EXCESS_DESTINATION"), vm.envAddress("SWAP_FACILITY"), vm.envAddress("WRAPPED_M_MIGRATION_ADMIN") ); diff --git a/script/DeployBase.sol b/script/DeployBase.sol index 07efa5c..4ca252c 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -9,12 +9,27 @@ import { WrappedMTokenMigratorV1 } from "../src/WrappedMTokenMigratorV1.sol"; import { WrappedMToken } from "../src/WrappedMToken.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 of the M Token contract. * @param registrar_ The address of the Registrar contract. * @param swapFacility_ The address of the SwapFacility contract. - * @param excessDestination_ The address of the excess destination. * @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. @@ -22,12 +37,11 @@ contract DeployBase { function deploy( address mToken_, address registrar_, - address excessDestination_, address swapFacility_, address wrappedMMigrationAdmin_ ) public virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenProxy_) { wrappedMTokenImplementation_ = address( - new WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) + new WrappedMToken(mToken_, registrar_, swapFacility_, wrappedMMigrationAdmin_) ); wrappedMTokenProxy_ = address(new Proxy(wrappedMTokenImplementation_)); @@ -41,9 +55,7 @@ contract DeployBase { * @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 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 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. */ @@ -54,25 +66,35 @@ contract DeployBase { address swapFacility_, address wrappedMMigrationAdmin_, address[] memory earners_, - address admin_, - address freezeManager_, - address pauser_, - address forcedTransferManager_ + UpgradeRoles memory roles_ ) public virtual returns (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) { wrappedMTokenImplementation_ = address( - new WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, wrappedMMigrationAdmin_) + new WrappedMToken(mToken_, registrar_, swapFacility_, wrappedMMigrationAdmin_) ); - wrappedMTokenMigrator_ = address( - new WrappedMTokenMigratorV1( - wrappedMTokenImplementation_, - earners_, - admin_, - freezeManager_, - pauser_, - forcedTransferManager_ - ) - ); + wrappedMTokenMigrator_ = _deployMigrator(wrappedMTokenImplementation_, earners_, excessDestination_, roles_); + } + + /// @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_) { + return + address( + new WrappedMTokenMigratorV1( + implementation_, + earners_, + roles_.admin, + roles_.freezeManager, + roles_.pauser, + roles_.forcedTransferManager, + roles_.excessManager, + excessDestination_ + ) + ); } /** diff --git a/script/DeployUpgradeMainnet.s.sol b/script/DeployUpgradeMainnet.s.sol index 6f89b72..8c76a34 100644 --- a/script/DeployUpgradeMainnet.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -30,9 +30,6 @@ contract DeployUpgradeMainnet is Script, DeployBase { // NOTE: Ensure this is the correct Migration Admin mainnet address. address internal constant _WRAPPED_M_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; - // NOTE: Ensure this is the correct Migration Admin mainnet address. - address internal constant _EARNER_MANAGER_MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; - address internal constant _WRAPPED_M_PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; // Mainnet address for the Proxy. // NOTE: Ensure this is the correct admin to use. @@ -47,6 +44,9 @@ contract DeployUpgradeMainnet is Script, DeployBase { // 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; @@ -113,10 +113,13 @@ contract DeployUpgradeMainnet is Script, DeployBase { _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, earners_, - _ADMIN, - _FREEZE_MANAGER, - _PAUSER, - _FORCED_TRANSFER_MANAGER + UpgradeRoles({ + admin: _ADMIN, + freezeManager: _FREEZE_MANAGER, + pauser: _PAUSER, + forcedTransferManager: _FORCED_TRANSFER_MANAGER, + excessManager: _EXCESS_MANAGER + }) ); vm.stopBroadcast(); diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 4191331..7fbb503 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -64,6 +64,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /// @inheritdoc IWrappedMToken bytes32 public constant EARNERS_LIST_NAME = "earners"; + /// @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"; @@ -79,9 +82,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /// @inheritdoc IWrappedMToken address public immutable registrar; - /// @inheritdoc IWrappedMToken - address public immutable excessDestination; - /// @inheritdoc IWrappedMToken address public immutable swapFacility; @@ -105,6 +105,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, mapping(address account => address claimRecipient) internal _claimRecipients; + /// @inheritdoc IWrappedMToken + address public excessDestination; + /* ============ Modifiers ============ */ /// @dev Modifier to check if caller is SwapFacility. @@ -118,22 +121,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /** * @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 excessDestination_ The address of an excess destination. - * @param swapFacility_ The address of a Swap Facility. - * @param migrationAdmin_ The address of a migration admin. + * @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 registrar_, - address excessDestination_, address swapFacility_, address migrationAdmin_ ) ERC20Extended("M (Wrapped) by M0", "wM", 6) { if ((mToken = mToken_) == address(0)) revert ZeroMToken(); if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); - if ((excessDestination = excessDestination_) == address(0)) revert ZeroExcessDestination(); if ((swapFacility = swapFacility_) == address(0)) revert ZeroSwapFacility(); if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); } @@ -142,20 +142,29 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /** * @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 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 forcedTransferManager_, + address excessManager_, + address excessDestination_ ) public initializer { 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_); @@ -268,6 +277,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, emit ClaimRecipientSet(msg.sender, claimRecipient_); } + /// @inheritdoc IWrappedMToken + function setExcessDestination(address excessDestination_) external onlyRole(EXCESS_MANAGER_ROLE) { + _setExcessDestination(excessDestination_); + } + /* ============ Temporary Admin Migration ============ */ /// @inheritdoc IWrappedMToken @@ -376,6 +390,16 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /* ============ 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. diff --git a/src/WrappedMTokenMigratorV1.sol b/src/WrappedMTokenMigratorV1.sol index d13ff70..e4a8399 100644 --- a/src/WrappedMTokenMigratorV1.sol +++ b/src/WrappedMTokenMigratorV1.sol @@ -55,6 +55,8 @@ contract WrappedMTokenMigratorV1 { address public immutable freezeManager; address public immutable pauser; address public immutable forcedTransferManager; + address public immutable excessManager; + address public immutable excessDestination; constructor( address newImplementation_, @@ -62,7 +64,9 @@ contract WrappedMTokenMigratorV1 { address admin_, address freezeManager_, address pauser_, - address forcedTransferManager_ + address forcedTransferManager_, + address excessManager_, + address excessDestination_ ) { newImplementation = newImplementation_; @@ -72,6 +76,8 @@ contract WrappedMTokenMigratorV1 { freezeManager = freezeManager_; pauser = pauser_; forcedTransferManager = forcedTransferManager_; + excessManager = excessManager_; + excessDestination = excessDestination_; } fallback() external virtual { @@ -93,11 +99,13 @@ contract WrappedMTokenMigratorV1 { (bool success_, ) = address(this).call( abi.encodeWithSelector( - bytes4(keccak256("initialize(address,address,address,address)")), + bytes4(keccak256("initialize(address,address,address,address,address,address)")), admin, freezeManager, pauser, - forcedTransferManager + forcedTransferManager, + excessManager, + excessDestination ) ); diff --git a/src/interfaces/IEarnerManager.sol b/src/interfaces/IEarnerManager.sol deleted file mode 100644 index a98eb1f..0000000 --- a/src/interfaces/IEarnerManager.sol +++ /dev/null @@ -1,178 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.26; - -import { IMigratable } from "../../lib/common/src/interfaces/IMigratable.sol"; - -/** - * @title Earner Status Manager interface for setting and returning earner status for Wrapped M Token accounts. - * @author M0 Labs - */ -interface IEarnerManager is IMigratable { - /* ============ Events ============ */ - - /** - * @notice Emitted when the earner for `account` is set to `status`. - * @param account The account under which yield could generate. - * @param status Whether the account is set as an earner, according to the admin. - * @param admin The admin who set the details and who will collect the fee. - * @param feeRate The fee rate to be taken from the yield. - */ - event EarnerDetailsSet(address indexed account, bool indexed status, address indexed admin, uint16 feeRate); - - /* ============ Custom Errors ============ */ - - /// @notice Emitted when `account` is already in the earners list, so it cannot be added by an admin. - error AlreadyInRegistrarEarnersList(address account); - - /// @notice Emitted when the lengths of input arrays do not match. - error ArrayLengthMismatch(); - - /// @notice Emitted when the length of an input array is 0. - error ArrayLengthZero(); - - /// @notice Emitted when the earner details have already been set by an existing and active admin. - error EarnerDetailsAlreadySet(address account); - - /// @notice Emitted when the earners lists are ignored, thus not requiring admin to define earners. - error EarnersListsIgnored(); - - /// @notice Emitted when the fee rate provided is too high (higher than 100% in basis points). - error FeeRateTooHigh(); - - /// @notice Emitted when setting fee rate to a nonzero value while setting status to false. - error InvalidDetails(); - - /// @notice Emitted when the caller is not an admin. - error NotAdmin(); - - /// @notice Emitted when the non-governance migrate function is called by an account other than the migration admin. - error UnauthorizedMigration(); - - /// @notice Emitted when an account (whose status is being set) is 0x0. - error ZeroAccount(); - - /// @notice Emitted in constructor if Migration Admin is 0x0. - error ZeroMigrationAdmin(); - - /// @notice Emitted in constructor if Registrar is 0x0. - error ZeroRegistrar(); - - /* ============ Interactive Functions ============ */ - - /** - * @notice Sets the status for `account` to `status`. - * @notice If approving an earner that is already earning, but was recently removed from the Registrar earners list, - * call `wrappedM.stopEarning(account)` before calling this, then call `wrappedM.startEarning(account)`. - * @param account The account under which yield could generate. - * @param status Whether the account is an earner, according to the admin. - * @param feeRate The fee rate to be taken from the yield. - */ - function setEarnerDetails(address account, bool status, uint16 feeRate) external; - - /** - * @notice Sets the status for multiple accounts. - * @notice If approving an earner that is already earning, but was recently removed from the Registrar earners list, - * call `wrappedM.stopEarning(account)` before calling this, then call `wrappedM.startEarning(account)`. - * @param accounts The accounts under which yield could generate. - * @param statuses Whether each account is an earner, respectively, according to the admin. - * @param feeRates The fee rates to be taken from the yield, respectively. - */ - function setEarnerDetails( - address[] calldata accounts, - bool[] calldata statuses, - uint16[] calldata feeRates - ) external; - - /* ============ Temporary Admin Migration ============ */ - - /** - * @notice Performs an arbitrarily defined migration. - * @param migrator The address of a migrator contract. - */ - function migrate(address migrator) external; - - /* ============ View/Pure Functions ============ */ - - /// @notice Maximum fee rate that can be set (100% in basis points). - function MAX_FEE_RATE() external pure returns (uint16 maxFeeRate); - - /// @notice Registrar name of admins list. - function ADMINS_LIST_NAME() external pure returns (bytes32 adminsListName); - - /// @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 name of earners list. - function EARNERS_LIST_NAME() external pure returns (bytes32 earnersListName); - - /// @notice Registrar key prefix to determine the migrator contract. - function MIGRATOR_KEY_PREFIX() external pure returns (bytes32 migratorKeyPrefix); - - /** - * @notice Returns the earner status for `account`. - * @param account The account being queried. - * @return status Whether the account is an earner. - */ - function earnerStatusFor(address account) external view returns (bool status); - - /** - * @notice Returns the statuses for multiple accounts. - * @param accounts The accounts being queried. - * @return statuses Whether each account is an earner, respectively. - */ - function earnerStatusesFor(address[] calldata accounts) external view returns (bool[] memory statuses); - - /** - * @notice Returns whether the lists of earners can be ignored (thus making all accounts earners). - * @return ignored Whether the lists of earners can be ignored. - */ - function earnersListsIgnored() external view returns (bool ignored); - - /** - * @notice Returns whether `account` is a Registrar-approved earner. - * @param account The account being queried. - * @return isInList Whether the account is a Registrar-approved earner. - */ - function isInRegistrarEarnersList(address account) external view returns (bool isInList); - - /** - * @notice Returns whether `account` is an Admin-approved earner. - * @param account The account being queried. - * @return isInList Whether the account is an Admin-approved earner. - */ - function isInAdministratedEarnersList(address account) external view returns (bool isInList); - - /** - * @notice Returns the earner details for `account`. - * @param account The account being queried. - * @return status Whether the account is an earner. - * @return feeRate The fee rate to be taken from the yield. - * @return admin The admin who set the details and who will collect the fee. - */ - function getEarnerDetails(address account) external view returns (bool status, uint16 feeRate, address admin); - - /** - * @notice Returns the earner details for multiple accounts, according to an admin. - * @param accounts The accounts being queried. - * @return statuses Whether each account is an earner, respectively. - * @return feeRates The fee rates to be taken from the yield, respectively. - * @return admins The admin who set the details and who will collect the fee, respectively. - */ - function getEarnerDetails( - address[] calldata accounts - ) external view returns (bool[] memory statuses, uint16[] memory feeRates, address[] memory admins); - - /** - * @notice Returns whether `account` is an admin. - * @param account The address of an account. - * @return isAdmin Whether the account is an admin. - */ - function isAdmin(address account) external view returns (bool isAdmin); - - /// @notice The account that can bypass the Registrar and call the `migrate(address migrator)` function. - function migrationAdmin() external view returns (address migrationAdmin); - - /// @notice Returns the address of the Registrar. - function registrar() external view returns (address); -} diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index a690926..23e1985 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -45,6 +45,12 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ 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. @@ -91,7 +97,10 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted in constructor if default admin is 0x0. error ZeroAdmin(); - /// @notice Emitted in constructor if Excess Destination is 0x0. + /// @notice Emitted in `initialize` if Earner 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. @@ -176,6 +185,13 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ 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 ============ */ /** @@ -189,6 +205,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice 100% in basis points. function HUNDRED_PERCENT() external pure returns (uint16 hundredPercent); + /// @notice The role that can set the excess destination and manage approved earners. + 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); diff --git a/test/harness/WrappedMTokenHarness.sol b/test/harness/WrappedMTokenHarness.sol index 5a0c1ed..fb370ee 100644 --- a/test/harness/WrappedMTokenHarness.sol +++ b/test/harness/WrappedMTokenHarness.sol @@ -8,10 +8,9 @@ contract WrappedMTokenHarness is WrappedMToken { constructor( address mToken_, address registrar_, - address excessDestination_, address swapFacility_, address migrationAdmin_ - ) WrappedMToken(mToken_, registrar_, excessDestination_, swapFacility_, migrationAdmin_) {} + ) WrappedMToken(mToken_, registrar_, swapFacility_, migrationAdmin_) {} function internalWrap(address account_, address recipient_, uint240 amount_) external { _wrap(account_, recipient_, amount_); diff --git a/test/integration/Deploy.t.sol b/test/integration/Deploy.t.sol index dd92eaf..b008ad6 100644 --- a/test/integration/Deploy.t.sol +++ b/test/integration/Deploy.t.sol @@ -34,7 +34,6 @@ contract DeployTests is Test, DeployBase { (address wrappedMTokenImplementation_, address wrappedMTokenProxy_) = deploy( _M_TOKEN, _REGISTRAR, - _EXCESS_DESTINATION, _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN ); @@ -45,7 +44,6 @@ contract DeployTests is Test, DeployBase { assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); - assertEq(IWrappedMToken(wrappedMTokenImplementation_).excessDestination(), _EXCESS_DESTINATION); assertEq(IWrappedMToken(wrappedMTokenImplementation_).swapFacility(), _SWAP_FACILITY); // Wrapped M Token Proxy assertions @@ -53,7 +51,6 @@ contract DeployTests is Test, DeployBase { assertEq(IWrappedMToken(wrappedMTokenProxy_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(wrappedMTokenProxy_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenProxy_).registrar(), _REGISTRAR); - assertEq(IWrappedMToken(wrappedMTokenProxy_).excessDestination(), _EXCESS_DESTINATION); assertEq(IWrappedMToken(wrappedMTokenProxy_).swapFacility(), _SWAP_FACILITY); assertEq(IWrappedMToken(wrappedMTokenProxy_).implementation(), wrappedMTokenImplementation_); } diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 0d427cb..12286cf 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -56,6 +56,7 @@ contract TestBase is Test { 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"); @@ -193,7 +194,7 @@ contract TestBase is Test { function _deployV2Components() internal { _wrappedMTokenImplementationV2 = address( - new WrappedMToken(address(_mToken), _registrar, _excessDestination, _swapFacility, _migrationAdmin) + new WrappedMToken(address(_mToken), _registrar, _swapFacility, _migrationAdmin) ); address[] memory earners_ = new address[](_earners.length); @@ -209,7 +210,9 @@ contract TestBase is Test { _admin, _freezeManager, _pauser, - _forcedTransferManager + _forcedTransferManager, + _excessManager, + _excessDestination ) ); } diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 990f4cb..8c43873 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -33,6 +33,7 @@ contract UpgradeTests is Test, DeployBase { 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, @@ -87,10 +88,13 @@ contract UpgradeTests is Test, DeployBase { _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, earners_, - _ADMIN, - _FREEZE_MANAGER, - _PAUSER, - _FORCED_TRANSFER_MANAGER + UpgradeRoles({ + admin: _ADMIN, + freezeManager: _FREEZE_MANAGER, + pauser: _PAUSER, + forcedTransferManager: _FORCED_TRANSFER_MANAGER, + excessManager: _EXCESS_MANAGER + }) ); vm.stopPrank(); @@ -99,7 +103,6 @@ contract UpgradeTests is Test, DeployBase { assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); - assertEq(IWrappedMToken(wrappedMTokenImplementation_).excessDestination(), _EXCESS_DESTINATION); assertEq(IWrappedMToken(wrappedMTokenImplementation_).swapFacility(), _SWAP_FACILITY); // Migrator assertions @@ -134,10 +137,20 @@ contract UpgradeTests is Test, DeployBase { _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); + _WRAPPED_M_TOKEN.initialize( + _ADMIN, + _FREEZE_MANAGER, + _PAUSER, + _FORCED_TRANSFER_MANAGER, + _EXCESS_MANAGER, + _EXCESS_DESTINATION + ); // Relevant storage slots. assertEq(_WRAPPED_M_TOKEN.totalEarningSupply(), totalEarningSupply_); diff --git a/test/unit/Migrations.t.sol b/test/unit/Migrations.t.sol index 51bdaa0..04787f3 100644 --- a/test/unit/Migrations.t.sol +++ b/test/unit/Migrations.t.sol @@ -17,16 +17,27 @@ contract Foo { 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_) public { + 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_; } } @@ -43,6 +54,7 @@ contract MigrationTests is Test { 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"); @@ -54,7 +66,7 @@ contract MigrationTests is Test { address mToken_ = makeAddr("mToken"); address implementation_ = address( - new WrappedMToken(address(mToken_), address(registrar_), _excessDestination, _swapFacility, _migrationAdmin) + new WrappedMToken(address(mToken_), address(registrar_), _swapFacility, _migrationAdmin) ); address proxy_ = address(new Proxy(address(implementation_))); @@ -65,7 +77,9 @@ contract MigrationTests is Test { _admin, _freezeManager, _pauser, - _forcedTransferManager + _forcedTransferManager, + _excessManager, + _excessDestination ) ); @@ -81,6 +95,8 @@ contract MigrationTests is Test { 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 { @@ -88,7 +104,7 @@ contract MigrationTests is Test { address mToken_ = makeAddr("mToken"); address implementation_ = address( - new WrappedMToken(address(mToken_), address(registrar_), _excessDestination, _swapFacility, _migrationAdmin) + new WrappedMToken(address(mToken_), address(registrar_), _swapFacility, _migrationAdmin) ); address proxy_ = address(new Proxy(address(implementation_))); @@ -99,7 +115,9 @@ contract MigrationTests is Test { _admin, _freezeManager, _pauser, - _forcedTransferManager + _forcedTransferManager, + _excessManager, + _excessDestination ) ); @@ -114,5 +132,7 @@ contract MigrationTests is Test { 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/unit/Stories.t.sol b/test/unit/Stories.t.sol index ef998f2..1511518 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -24,8 +24,13 @@ contract StoryTests is Test { 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 _pauser = makeAddr("pauser"); MockM internal _mToken; MockRegistrar internal _registrar; @@ -43,12 +48,19 @@ contract StoryTests is Test { _implementation = new WrappedMToken( address(_mToken), address(_registrar), - _excessDestination, address(_swapFacility), _migrationAdmin ); _wrappedMToken = IWrappedMToken(address(new Proxy(address(_implementation)))); + WrappedMToken(address(_wrappedMToken)).initialize( + _admin, + _freezeManager, + _pauser, + _forcedTransferManager, + _excessManager, + _excessDestination + ); } function test_story() external { diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 5ffa2be..d09c0b5 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -47,13 +47,19 @@ contract WrappedMTokenTests is BaseUnitTest { _implementation = new WrappedMTokenHarness( address(_mToken), address(_registrar), - _excessDestination, address(_swapFacility), _migrationAdmin ); _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); - _wrappedMToken.initialize(_admin, _freezeManager, _pauser, _forcedTransferManager); + _wrappedMToken.initialize( + _admin, + _freezeManager, + _pauser, + _forcedTransferManager, + _excessManager, + _excessDestination + ); } /* ============ constants ============ */ @@ -81,33 +87,22 @@ contract WrappedMTokenTests is BaseUnitTest { function test_constructor_zeroMToken() external { vm.expectRevert(IWrappedMToken.ZeroMToken.selector); - new WrappedMTokenHarness(address(0), address(0), address(0), address(0), address(0)); + 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), address(0)); - } - - function test_constructor_zeroExcessDestination() external { - vm.expectRevert(IWrappedMToken.ZeroExcessDestination.selector); - new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0), address(0)); + 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), _excessDestination, address(0), address(0)); + 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), - _excessDestination, - address(_swapFacility), - address(0) - ); + new WrappedMTokenHarness(address(_mToken), address(_registrar), address(_swapFacility), address(0)); } function test_constructor_zeroImplementation() external { @@ -124,34 +119,124 @@ contract WrappedMTokenTests is BaseUnitTest { 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); + 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); + 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); + 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)); + 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 ============ */ diff --git a/test/utils/BaseUnitTest.sol b/test/utils/BaseUnitTest.sol index 768df6c..93992a7 100644 --- a/test/utils/BaseUnitTest.sol +++ b/test/utils/BaseUnitTest.sol @@ -19,6 +19,7 @@ contract BaseUnitTest is Test { 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"; @@ -28,6 +29,7 @@ contract BaseUnitTest is Test { 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"); From ef53e13c48907b78527b5abbee41a39ec4fda638 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 10 Jun 2026 15:25:22 -0500 Subject: [PATCH 23/27] chore(lib): update common to v1.5.2 --- .gitmodules | 2 +- foundry.lock | 4 ++-- lib/common | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index 01078a7..1bc2050 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,7 +5,7 @@ [submodule "lib/common"] path = lib/common url = https://github.com/m0-foundation/common - branch = release-v1.5.1 + 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/foundry.lock b/foundry.lock index cc08885..65f2d0a 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,8 +1,8 @@ { "lib/common": { "branch": { - "name": "release-v1.5.1", - "rev": "20440d03de21fe62970829245872e9adae6cd822" + "name": "release-v1.5.2", + "rev": "489fae6daecd227616155e4f8cacfd17aeb3e2a1" } }, "lib/evm-m-extensions": { diff --git a/lib/common b/lib/common index 20440d0..489fae6 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 20440d03de21fe62970829245872e9adae6cd822 +Subproject commit 489fae6daecd227616155e4f8cacfd17aeb3e2a1 From 145a681ea86939bb5d8f4640619767607bb8ca51 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 12 Jun 2026 15:28:20 -0500 Subject: [PATCH 24/27] fix(WrappedM): address internal review findings (I-01, I-02, I-03) (#132) - I-01: call _disableInitializers() in constructor to lock down the implementation contract, per OpenZeppelin guidance - I-02: fix NatSpec typos in IWrappedMToken (Earner Manager -> Registrar/ Excess Manager, EXCESS_MANAGER_ROLE description) - I-03: remove unused HUNDRED_PERCENT constant and interface declaration --- src/WrappedMToken.sol | 6 +++--- src/interfaces/IWrappedMToken.sol | 15 ++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 7fbb503..3b7f5e6 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -55,9 +55,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /* ============ Variables ============ */ - /// @inheritdoc IWrappedMToken - uint16 public constant HUNDRED_PERCENT = 10_000; - /// @inheritdoc IWrappedMToken bytes32 public constant EARNERS_LIST_IGNORED_KEY = "earners_list_ignored"; @@ -119,6 +116,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, /* ============ 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. @@ -132,6 +130,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, 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(); diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 23e1985..bfe8f7d 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -97,7 +97,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted in constructor if default admin is 0x0. error ZeroAdmin(); - /// @notice Emitted in `initialize` if Earner Manager is 0x0. + /// @notice Emitted in `initialize` if Excess Manager is 0x0. error ZeroExcessManager(); /// @notice Emitted in `initialize` and `setExcessDestination` if Excess Destination is 0x0. @@ -156,25 +156,25 @@ interface IWrappedMToken is IMigratable, IERC20Extended { function disableEarning() external; /** - * @notice Starts earning for `account` if allowed by the Earner Manager. + * @notice Starts earning for `account` if allowed by the Registrar. * @param account The account to start earning for. */ function startEarningFor(address account) external; /** - * @notice Starts earning for multiple accounts if individually allowed by the Earner Manager. + * @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 Earner Manager. + * @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 Earner Manager. + * @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; @@ -202,10 +202,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /* ============ View/Pure Functions ============ */ - /// @notice 100% in basis points. - function HUNDRED_PERCENT() external pure returns (uint16 hundredPercent); - - /// @notice The role that can set the excess destination and manage approved earners. + /// @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. From aa6dc9d2e8a7b5df7d583964253ae68190761ce2 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 19 Jun 2026 15:16:43 -0500 Subject: [PATCH 25/27] fix(WrappedM): address ChainSecurity audit issues (#133) * fix(WrappedM): correct Claimed event recipient when skipTransfer is true When `_claim` runs with `skipTransfer_` true (paused stop-earning or freeze), the yield stays on `account_`, but the `Claimed` event reported the configured claim recipient. Compute the effective recipient as `account_` when transfer is skipped so the event reflects where the yield actually lands. * docs(WrappedM): correct inaccurate NatSpec on unwrap and earning toggles - unwrap: document that `recipient` is unused and M is always sent to the SwapFacility, which routes it to the final recipient. - enableEarning/disableEarning: replace "if it has never been done" with "not already enabled/disabled", since earning can be toggled multiple times. * chore(WrappedM): call inherited base initializers in initialize() Invoke __Context_init, __ERC165_init, and __AccessControl_init in the initializer. These are no-ops in the current OpenZeppelin version, but calling them prevents silently uninitialized state should a future version add meaningful initialization logic. --- src/WrappedMToken.sol | 9 ++++-- src/interfaces/IWrappedMToken.sol | 8 ++--- test/unit/WrappedMToken.t.sol | 52 +++++++++++++++++++++---------- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 3b7f5e6..5e77268 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -157,6 +157,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, address excessManager_, address excessDestination_ ) public initializer { + __Context_init(); + __ERC165_init(); + __AccessControl_init(); + if (admin_ == address(0)) revert ZeroAdmin(); _grantRole(DEFAULT_ADMIN_ROLE, admin_); @@ -551,13 +555,14 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, totalEarningSupply += yield_; } - address claimRecipient_ = claimRecipientFor(account_); + // 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 (skipTransfer_ || claimRecipient_ == account_) return yield_; + if (claimRecipient_ == account_) return yield_; _transfer(account_, claimRecipient_, yield_, currentIndex_); } diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index bfe8f7d..ba4700a 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -129,9 +129,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { function wrap(address recipient, uint256 amount) external; /** - * @notice Unwraps `amount` wM from the caller into M for `recipient`. + * @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 The account receiving the withdrawn M. + * @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. */ function unwrap(address recipient, uint256 amount) external; @@ -149,10 +149,10 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ function claimExcess() external returns (uint240 claimed); - /// @notice Enables earning of Wrapped M if allowed by the Registrar 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 of Wrapped M if disallowed by the Registrar 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; /** diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index d09c0b5..3f91df6 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -169,14 +169,7 @@ contract WrappedMTokenTests is BaseUnitTest { WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); vm.expectRevert(IForcedTransferable.ZeroForcedTransferManager.selector); - wrappedMToken_.initialize( - _admin, - _freezeManager, - _pauser, - address(0), - _excessManager, - _excessDestination - ); + wrappedMToken_.initialize(_admin, _freezeManager, _pauser, address(0), _excessManager, _excessDestination); } function test_initialize_zeroExcessManager() external { @@ -197,14 +190,7 @@ contract WrappedMTokenTests is BaseUnitTest { WrappedMTokenHarness wrappedMToken_ = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); vm.expectRevert(IWrappedMToken.ZeroExcessDestination.selector); - wrappedMToken_.initialize( - _admin, - _freezeManager, - _pauser, - _forcedTransferManager, - _excessManager, - address(0) - ); + wrappedMToken_.initialize(_admin, _freezeManager, _pauser, _forcedTransferManager, _excessManager, address(0)); } /* ============ setExcessDestination ============ */ @@ -1628,6 +1614,38 @@ contract WrappedMTokenTests is BaseUnitTest { 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); @@ -1860,7 +1878,7 @@ contract WrappedMTokenTests is BaseUnitTest { _wrappedMToken.freeze(_bob); vm.expectEmit(); - emit IWrappedMToken.Claimed(_alice, _bob, 100); + emit IWrappedMToken.Claimed(_alice, _alice, 100); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, 100); From 47165968dec656961e1c0b75e76efe15fb227c94 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Wed, 24 Jun 2026 08:58:08 -0500 Subject: [PATCH 26/27] fix(WrappedM): address remaining ChainSecurity audit issues (#134) * fix(WrappedM): prevent frozen claim recipient from blocking stopEarningFor An earner could point their claim recipient at a frozen address so that the permissionless stopEarningFor() reverts inside _claim() -> _transfer() -> _revertIfFrozen(recipient_), keeping the account earning indefinitely and defeating the deauthorization mechanism. Both stopEarningFor() overloads now fall back to skipTransfer when the resolved claim recipient is frozen, mirroring the _beforeFreeze path: the yield is crystallized onto the account's own balance instead of being routed to the frozen recipient, so deauthorization always succeeds. * fix(WrappedM): enforce sorted unique earners in migrator list The WrappedMTokenMigratorV1 migration corrupts accounting if an address appears twice in the earners array, since the first pass overwrites the lastIndex slot with the computed principal. ListOfEarnersToMigrate now requires strictly ascending addresses, enforcing sorted order, uniqueness and non-zero in a single O(n) pass without the gas cost of an O(n^2) check. Integration test fixtures sort the in-memory earners copy before deploying the migrator, mirroring the production generation script. * chore(script): harden earners list generation Make the earners list generation safe to rely on for the v1->v2 migration: - Paginate holder fetches so earners are never silently truncated by the `first` cap, with a max-pages guard against a broken `skip`. - Build the whole file in memory and write once after every network succeeds; abort without writing on any error, and exit non-zero, so a partial run never mixes fresh and stale lists. - Throw on duplicate or zero earner addresses instead of relying solely on the on-chain check. - Fix the GraphQL response typing (drop the unsound cast), add a request timeout, centralize the chain enum, and drop the unused balance field. * Sort earners before migrator construction (#135) * fix(deploy): sort earners before migrator construction * refactor(test): pass earners storage array directly to migrator deploy Drop the manual storage-to-memory copy loops in the integration test fixtures; assigning or passing the storage array to a memory parameter already performs the copy. --------- Co-authored-by: Pierrick Turelier --------- Co-authored-by: Denali Marsh --- script/DeployBase.sol | 18 ++ script/EarnersAddresses.sol | 34 +-- script/generate-earners-array.ts | 298 ++++++++++++------------- src/ListOfEarnersToMigrate.sol | 16 ++ src/WrappedMToken.sol | 9 +- test/integration/TestBase.sol | 33 ++- test/integration/Upgrade.t.sol | 8 +- test/unit/ListOfEarnersToMigrate.t.sol | 90 ++++++++ test/unit/WrappedMToken.t.sol | 66 +++++- 9 files changed, 373 insertions(+), 199 deletions(-) create mode 100644 test/unit/ListOfEarnersToMigrate.t.sol diff --git a/script/DeployBase.sol b/script/DeployBase.sol index 4ca252c..61494a2 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -82,6 +82,8 @@ contract DeployBase { address excessDestination_, UpgradeRoles memory roles_ ) internal returns (address migrator_) { + earners_ = _sortAddresses(earners_); + return address( new WrappedMTokenMigratorV1( @@ -97,6 +99,22 @@ contract DeployBase { ); } + 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_; + } + /** * @dev Mock deploys Wrapped M Token, returning the would-be addresses. * @param deployer_ The address of the deployer. diff --git a/script/EarnersAddresses.sol b/script/EarnersAddresses.sol index a138dcf..2d0cb9a 100644 --- a/script/EarnersAddresses.sol +++ b/script/EarnersAddresses.sol @@ -17,30 +17,30 @@ library EarnersAddresses { } function getEthereumEarners() internal pure returns (address[] memory) { address[24] memory earners = [ - 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, - 0xfF95c5f35F4ffB9d5f596F898ac1ae38D62749c2, 0x0502d65f26f45d17503E4d34441F5e73Ea143033, - 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, + 0x13Ccb6E28F22E2f6783BaDedCe32cc74583A3647, + 0x48Afe17cB6363fD1aaeA50a8CB652C5978972c96, + 0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470, + 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec, + 0x7db685961F97c847A4C815D43E9cc0E5647328b9, 0x81ad394C0Fa87e99Ca46E1aca093BEe020f203f4, - 0xfE940BFE535013a52e8e2DF9644f95E3C94fa14B, - 0xE0663f2372cAa1459b7ade90812Dc737CE587FA6, - 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, + 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, + 0x985DE23260743c2c2f09BFdeC50b048C7a18c461, 0x9c6e67fA86138Ab49359F595BfE4Fb163D0f16cc, - 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, + 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70, + 0xa969cFCd9e583edb8c8B270Dc8CaFB33d6Cf662D, + 0xB50A1f651A5ACb2679c8f679D782c728f3702E53, 0xB65a66621D7dE34afec9b9AC0755133051550dD7, + 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, 0xcAD001c30E96765aC90307669d578219D4fb1DCe, + 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, + 0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7, 0xDeD796De6a14E255487191963dEe436c45995813, - 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90, - 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb, - 0x13Ccb6E28F22E2f6783BaDedCe32cc74583A3647, - 0x985DE23260743c2c2f09BFdeC50b048C7a18c461, - 0x7db685961F97c847A4C815D43E9cc0E5647328b9, - 0x48Afe17cB6363fD1aaeA50a8CB652C5978972c96, + 0xE0663f2372cAa1459b7ade90812Dc737CE587FA6, 0xE72Fe64840F4EF80E3Ec73a1c749491b5c938CB9, - 0xCF3166181848eEC4Fd3b9046aE7CB582F34d2e6c, - 0xB50A1f651A5ACb2679c8f679D782c728f3702E53, - 0x9F6d1a62bf268Aa05a1218CFc89C69833D2d2a70, - 0x569D7dccBF6923350521ecBC28A555A500c4f0Ec + 0xea0C048c728578b1510EBDF9b692E8936D6Fbc90, + 0xfE940BFE535013a52e8e2DF9644f95E3C94fa14B, + 0xfF95c5f35F4ffB9d5f596F898ac1ae38D62749c2 ]; address[] memory result = new address[](24); for (uint256 i = 0; i < 24; ++i) { diff --git a/script/generate-earners-array.ts b/script/generate-earners-array.ts index 4e0ac46..24f0977 100755 --- a/script/generate-earners-array.ts +++ b/script/generate-earners-array.ts @@ -1,76 +1,150 @@ #!/usr/bin/env tsx -import { readFileSync, writeFileSync } from "fs"; +import { writeFileSync } from "fs"; import { join } from "path"; import { getAddress } from "ethers"; interface WMHolder { address: string; - balance: string; isEarning: boolean; } interface GraphQLResponse { - data?: { - WMHolders?: WMHolder[]; - WMHoldersArbitrum?: WMHolder[]; - }; + 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 = network === "ethereum" ? "WMHolders" : "WMHoldersL2"; + const fieldName = fieldNameFor(network); const chainArg = - network === "ethereum" ? "" : `chain: ${network.toUpperCase()}`; - - const query = ` - query GetWMHolders { - ${fieldName}(first: 1000, ${chainArg}) { - address - balance - isEarning + 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 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(); + const result = await graphqlRequest(query); + const pageHolders = result.data?.[fieldName] ?? []; - if (result.errors) { - throw new Error( - `GraphQL errors: ${result.errors.map((e) => e.message).join(", ")}`, - ); + holders.push(...pageHolders); + + if (pageHolders.length < PAGE_SIZE) return holders; } - return result.data?.[fieldName as keyof GraphQLResponse["data"]] || []; + throw new Error( + `Exceeded ${MAX_PAGES} pages (${MAX_PAGES * PAGE_SIZE} holders) fetching ${network}; pagination may be broken`, + ); } -function generateFunctionName(network: string): string { - return `get${network.charAt(0).toUpperCase() + network.slice(1)}Earners`; +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, - earners: WMHolder[], + addresses: string[], ): string { const functionName = generateFunctionName(network); - const earnerAddresses = earners.map((e) => getAddress(e.address)); - const arrayLength = earnerAddresses.length; + const arrayLength = addresses.length; - const addressesLines = earnerAddresses + const addressesLines = addresses .map((addr) => ` ${addr}`) .join(",\n"); @@ -87,129 +161,55 @@ ${addressesLines} `; } -function getOrCreateSolidityFile(): string { - const outputPath = join("script", "EarnersAddresses.sol"); - - try { - return readFileSync(outputPath, "utf-8"); - } catch (error) { - return `// SPDX-License-Identifier: UNLICENSED +function buildSolidityFile(functions: string[]): string { + return `// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; library EarnersAddresses { -} +${functions.join("")}} `; - } } -function updateSolidityFile(network: string, earners: WMHolder[]): void { - const outputPath = join("script", "EarnersAddresses.sol"); - let content = getOrCreateSolidityFile(); - const functionName = generateFunctionName(network); - - const newFunction = generateSolidityFunction(network, earners); - - if (content.includes(functionName)) { - const functionStart = content.indexOf(`function ${functionName}`); - if (functionStart === -1) { - throw new Error(`Could not find function ${functionName}`); - } - - const openBrace = content.indexOf("{", functionStart); - if (openBrace === -1) { - throw new Error(`Could not find opening brace for ${functionName}`); - } - - let braceCount = 0; - let functionEnd = -1; - for (let i = openBrace; i < content.length; i++) { - if (content[i] === "{") braceCount++; - else if (content[i] === "}") { - braceCount--; - if (braceCount === 0) { - functionEnd = i; - break; - } - } - } - - if (functionEnd === -1) { - throw new Error(`Could not find closing brace for ${functionName}`); - } - - content = - content.slice(0, functionStart) + - newFunction + - content.slice(functionEnd + 1); - console.log(`Updated existing ${functionName}() function`); - } else { - const libraryEndIndex = content.lastIndexOf("}"); - if (libraryEndIndex === -1) { - throw new Error("Invalid Solidity file format"); - } - content = - content.slice(0, libraryEndIndex) + - newFunction + - content.slice(libraryEndIndex); - console.log(`Added new ${functionName}() function`); - } +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[] = []; - writeFileSync(outputPath, content); -} - -async function main() { - const networks = [ - "arbitrum", - "base", - "bsc", - "ethereum", - "hyperevm", - "linea", - "mantra", - "optimism", - "plasma", - "plume", - "soneium", - ]; - - for (const network of networks) { + for (const network of NETWORKS) { const networkName = network.charAt(0).toUpperCase() + network.slice(1); - try { - console.log(`\nFetching WrappedM holders from ${networkName}...`); + 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 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)`, - ); + 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; - } + if (earners.length === 0) { + console.log("No earners found, skipping"); + continue; + } - updateSolidityFile(network, earners); + const addresses = toSortedUniqueAddresses(earners, network); + functions.push(generateSolidityFunction(network, addresses)); - const functionName = generateFunctionName(network); - console.log( - `Added ${functionName}() with ${earners.length} checksummed addresses to EarnersAddresses.sol`, - ); - } catch (error) { - console.error( - `Error processing ${networkName}:`, - error instanceof Error ? error.message : error, - ); - } + 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(); +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 index 1fa76da..c0b3572 100644 --- a/src/ListOfEarnersToMigrate.sol +++ b/src/ListOfEarnersToMigrate.sol @@ -7,9 +7,25 @@ pragma solidity 0.8.26; * @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_; } diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 5e77268..9123ccf 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -257,20 +257,23 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, _revertIfFrozen(account_); _revertIfApprovedEarner(account_); - _stopEarningFor(account_, currentIndex(), paused()); + // 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_))); } /// @inheritdoc IWrappedMToken function stopEarningFor(address[] calldata accounts_) external { uint128 currentIndex_ = currentIndex(); - bool skipTransfer_ = paused(); + bool paused_ = paused(); FreezableStorageStruct storage $ = _getFreezableStorageLocation(); for (uint256 i; i < accounts_.length; ++i) { _revertIfFrozen($, accounts_[i]); _revertIfApprovedEarner(accounts_[i]); - _stopEarningFor(accounts_[i], currentIndex_, skipTransfer_); + _stopEarningFor(accounts_[i], currentIndex_, paused_ || isFrozen(claimRecipientFor(accounts_[i]))); } } diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 12286cf..733f558 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -12,13 +12,14 @@ import { Proxy } from "../../lib/common/src/Proxy.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; -import { WrappedMTokenMigratorV1 } from "../../src/WrappedMTokenMigratorV1.sol"; + +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 { +contract TestBase is Test, DeployBase { uint256 public mainnetFork; IMTokenLike internal constant _mToken = IMTokenLike(0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b); @@ -197,23 +198,17 @@ contract TestBase is Test { new WrappedMToken(address(_mToken), _registrar, _swapFacility, _migrationAdmin) ); - address[] memory earners_ = new address[](_earners.length); - - for (uint256 index_; index_ < _earners.length; ++index_) { - earners_[index_] = _earners[index_]; - } - - _wrappedMTokenMigratorV1 = address( - new WrappedMTokenMigratorV1( - _wrappedMTokenImplementationV2, - earners_, - _admin, - _freezeManager, - _pauser, - _forcedTransferManager, - _excessManager, - _excessDestination - ) + _wrappedMTokenMigratorV1 = _deployMigrator( + _wrappedMTokenImplementationV2, + _earners, + _excessDestination, + UpgradeRoles({ + admin: _admin, + freezeManager: _freezeManager, + pauser: _pauser, + forcedTransferManager: _forcedTransferManager, + excessManager: _excessManager + }) ); } diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 8c43873..b8eadf0 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -74,12 +74,6 @@ contract UpgradeTests is Test, DeployBase { _DEPLOYER_NONCE ); - address[] memory earners_ = new address[](_earners.length); - - for (uint256 index_; index_ < _earners.length; ++index_) { - earners_[index_] = _earners[index_]; - } - vm.startPrank(_DEPLOYER); (address wrappedMTokenImplementation_, address wrappedMTokenMigrator_) = deployUpgrade( _M_TOKEN, @@ -87,7 +81,7 @@ contract UpgradeTests is Test, DeployBase { _EXCESS_DESTINATION, _SWAP_FACILITY, _WRAPPED_M_MIGRATION_ADMIN, - earners_, + _earners, UpgradeRoles({ admin: _ADMIN, freezeManager: _FREEZE_MANAGER, 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/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 3f91df6..44a6fe8 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -1658,8 +1658,6 @@ contract WrappedMTokenTests is BaseUnitTest { } function test_stopEarningFor_frozenClaimRecipient() external { - _wrappedMToken.setIsEarningOf(_alice, true); - _mToken.setCurrentIndex(1_210000000000); _wrappedMToken.setEnableMIndex(1_100000000000); @@ -1669,13 +1667,28 @@ contract WrappedMTokenTests is BaseUnitTest { _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, so skipTransfer=false: _claim tries to transfer yield to frozen _bob → reverts. - vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, _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 { @@ -1822,6 +1835,51 @@ contract WrappedMTokenTests is BaseUnitTest { _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); From d2374bea6a02cbf9e9bad69a050fbabdbf48290e Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Mon, 29 Jun 2026 13:25:08 -0500 Subject: [PATCH 27/27] fix(WrappedM): chain OpenZeppelin PausableUpgradeable initializer Call OZ's parameterless `__Pausable_init()` in `initialize()` to complete the initializer chain. Distinct from the `Pausable` component's `__Pausable_init(pauser_)` overload, which only grants the pauser role. No behavioral change: OZ v5.3.0's `__Pausable_init()` is empty and the `_paused` flag already defaults to false. --- src/WrappedMToken.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index 9123ccf..9edf4cc 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -160,6 +160,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended, Freezable, __Context_init(); __ERC165_init(); __AccessControl_init(); + __Pausable_init(); if (admin_ == address(0)) revert ZeroAdmin(); _grantRole(DEFAULT_ADMIN_ROLE, admin_);