From 848f90c698d9b89c4ee6d1ef625dc4ef590157b3 Mon Sep 17 00:00:00 2001 From: Michael De Luca <35537333+deluca-mike@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:20:12 -0400 Subject: [PATCH] feat: Earner Manager --- script/Deploy.s.sol | 35 +- script/DeployBase.sol | 163 +++-- script/DeployUpgradeMainnet.s.sol | 47 +- src/EarnerManager.sol | 258 ++++++++ src/WrappedMToken.sol | 140 +++- ...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 | 58 +- 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 | 304 +++++++-- test/utils/EarnerManagerHarness.sol | 17 + test/utils/Mocks.sol | 20 + test/utils/WrappedMTokenHarness.sol | 29 +- 19 files changed, 1916 insertions(+), 287 deletions(-) create mode 100644 src/EarnerManager.sol rename src/{MigratorV1.sol => WrappedMTokenMigratorV1.sol} (95%) 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 c4e1325..6e37bd9 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -5,87 +5,164 @@ 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_ - ) public virtual returns (address implementation_, address migrator_) { - // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. - // Migrator needs `implementation_` addresses. + address wrappedMMigrationAdmin_, + address earnerManagerMigrationAdmin_ + ) + 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_)); + wrappedMTokenMigrator_ = address(new WrappedMTokenMigratorV1(wrappedMTokenImplementation_)); } /** * @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 060e563..033b36f 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); function run() external { address deployer_ = vm.rememberKey(vm.envUint("PRIVATE_KEY")); @@ -48,15 +51,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_; } @@ -73,13 +83,28 @@ contract DeployUpgradeMainnet is Script, DeployBase { if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); - (implementation_, migrator_) = deployUpgrade(_M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _MIGRATION_ADMIN); + ( + earnerManagerImplementation_, + earnerManagerProxy_, + wrappedMTokenImplementation_, + wrappedMTokenMigrator_ + ) = deployUpgrade( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _WRAPPED_M_MIGRATION_ADMIN, + _EARNER_MANAGER_MIGRATION_ADMIN + ); 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 d9f4d33..c9d0c95 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 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. + * @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 uint128 lastIndex; 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; @@ -98,17 +107,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(); } @@ -175,8 +187,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. @@ -194,8 +205,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(); @@ -215,11 +225,31 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { _startEarningFor(account_, _currentMIndex()); } + /// @inheritdoc IWrappedMToken + function startEarningFor(address[] calldata accounts_) external { + if (!isEarningEnabled()) revert EarningIsDisabled(); + + 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); @@ -472,10 +502,51 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { emit Claimed(account_, claimRecipient_, yield_); emit Transfer(address(0), account_, yield_); - if (claimRecipient_ != account_) { + uint240 yieldNetOfFees_ = yield_; + + if (accountInfo_.hasEarnerDetails) { + unchecked { + yieldNetOfFees_ -= _handleEarnerDetails(account_, yield_, currentIndex_); + } + } + + if ((claimRecipient_ != account_) && (yieldNetOfFees_ != 0)) { // NOTE: Watch out for a long chain of earning claim override recipients. - _transfer(account_, claimRecipient_, yield_, currentIndex_); + _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_, admin_, fee_, currentIndex_); } /** @@ -619,7 +690,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_]; @@ -627,6 +700,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { accountInfo_.isEarning = true; accountInfo_.lastIndex = currentIndex_; + accountInfo_.hasEarnerDetails = admin_ != address(0); // Has earner details if an admin exists for this account. uint240 balance_ = accountInfo_.balance; @@ -645,7 +719,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_); _claim(account_, currentIndex_); @@ -655,6 +731,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { delete accountInfo_.isEarning; delete accountInfo_.lastIndex; + delete accountInfo_.hasEarnerDetails; uint240 balance_ = accountInfo_.balance; @@ -674,6 +751,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; @@ -701,6 +785,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. @@ -738,17 +835,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. @@ -784,22 +870,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 95% rename from src/MigratorV1.sol rename to src/WrappedMTokenMigratorV1.sol index 74cef78..c7c84c9 100644 --- a/src/MigratorV1.sol +++ b/src/WrappedMTokenMigratorV1.sol @@ -6,7 +6,7 @@ pragma solidity 0.8.26; * @title Migrator contract for migrating a WrappedMToken contract from V1 to V2. * @author M^0 Labs */ -contract MigratorV1 { +contract WrappedMTokenMigratorV1 { /// @dev Storage slot with the address of the current factory. `keccak256('eip1967.proxy.implementation') - 1`. uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 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 22f672f..1133991 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. * @param account The account that is an approved earner. */ error IsApprovedEarner(address account); @@ -83,14 +83,17 @@ 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. * @param account The account that is not an approved earner. */ error NotApprovedEarner(address account); - /// @notice Emitted when the non-governance migrate function is called by a account other than the migration admin. + /// @notice Emitted when the non-governance migrate function is called by an account other than the migration admin. error UnauthorizedMigration(); + /// @notice Emitted in constructor if Earner Manager is 0x0. + error ZeroEarnerManager(); + /// @notice Emitted in constructor if Excess Destination is 0x0. error ZeroExcessDestination(); @@ -189,17 +192,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. @@ -216,6 +231,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); @@ -284,6 +302,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 523d731..5ad757f 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; function _getSource(address token_) internal pure returns (address source_) { if (token_ == _USDC) return _USDC_SOURCE; @@ -175,16 +179,18 @@ 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) ); - _migratorV1 = address(new MigratorV1(_implementationV2)); + _wrappedMTokenMigratorV1 = address(new WrappedMTokenMigratorV1(_wrappedMTokenImplementationV2)); } function _migrate() internal { _set( keccak256(abi.encode(_MIGRATOR_V1_PREFIX, address(_wrappedMToken))), - bytes32(uint256(uint160(_migratorV1))) + bytes32(uint256(uint160(_wrappedMTokenMigratorV1))) ); _wrappedMToken.migrate(); @@ -192,7 +198,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 b8e1fd5..c27e73c 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; @@ -21,35 +23,57 @@ 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); vm.startPrank(_DEPLOYER); - (address implementation_, address migrator_) = deployUpgrade( - _M_TOKEN, - _REGISTRAR, - _EXCESS_DESTINATION, - _MIGRATION_ADMIN - ); + ( + address earnerManagerImplementation_, + address earnerManagerProxy_, + address wrappedMTokenImplementation_, + address wrappedMTokenMigrator_ + ) = deployUpgrade( + _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); // Migrator assertions - assertEq(migrator_, expectedMigrator_); + assertEq(wrappedMTokenMigrator_, expectedWrappedMTokenMigrator_); 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_); } } 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 6d81210..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_))); - - _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/unit/Migrations.t.sol b/test/unit/Migrations.t.sol new file mode 100644 index 0000000..6baf7a9 --- /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()))); + + 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()))); + + 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 906a3a0..a8eb8ca 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(), 133_333336); assertEq(_wrappedMToken.excess(), 416_666664); - _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(), 66_666672); assertEq(_wrappedMToken.excess(), 416_666664); - _registrar.setListContains(_EARNERS_LIST_NAME, _carol, true); + _earnerManager.setEarnerDetails(_carol, true, 0, address(0)); _wrappedMToken.startEarningFor(_carol); @@ -364,8 +373,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); @@ -399,8 +408,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 c456131..8a3ff79 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -13,7 +13,7 @@ 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. @@ -23,6 +23,8 @@ import { WrappedMTokenHarness } from "../utils/WrappedMTokenHarness.sol"; 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"; @@ -39,6 +41,7 @@ contract WrappedMTokenTests is Test { uint128 internal _currentIndex; + MockEarnerManager internal _earnerManager; MockM internal _mToken; MockRegistrar internal _registrar; WrappedMTokenHarness internal _implementation; @@ -50,9 +53,12 @@ contract WrappedMTokenTests is Test { _mToken = new MockM(); _mToken.setCurrentIndex(_EXP_SCALED_ONE); + _earnerManager = new MockEarnerManager(); + _implementation = new WrappedMTokenHarness( address(_mToken), address(_registrar), + address(_earnerManager), _excessDestination, _migrationAdmin ); @@ -84,22 +90,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 { @@ -141,7 +164,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false, false); _mToken.setBalanceOf(_alice, 1_002); @@ -207,7 +230,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -275,7 +298,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -341,7 +364,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -407,7 +430,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -465,7 +488,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); _wrappedMToken.internalUnwrap(_alice, _alice, 1_000); @@ -507,7 +530,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setPrincipalOfTotalEarningSupply(909); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false, false); _mToken.setBalanceOf(address(_wrappedMToken), 1_000); @@ -560,7 +583,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -629,7 +652,7 @@ contract WrappedMTokenTests is Test { balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false, false); _wrappedMToken.setTotalEarningSupply(balance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -680,7 +703,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -705,7 +728,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -724,7 +747,100 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_bob), 100); } - function testFuzz_claimFor(uint240 balance_, uint128 accountIndex_, uint128 index_, bool claimOverride_) external { + function test_claimFor_earner_withFee() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, 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 { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, 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 { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _registrar.set( + keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), + bytes32(uint256(uint160(_charlie))) + ); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, 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( + uint240 balance_, + uint128 accountIndex_, + uint128 index_, + bool claimOverride_, + uint16 feeRate_ + ) 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)); @@ -742,7 +858,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningSupply(balance_); - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false, feeRate_ != 0); + + _earnerManager.setEarnerDetails(_alice, true, feeRate_, _bob); _mToken.setCurrentIndex(index_); @@ -756,6 +874,19 @@ 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)) { + vm.expectEmit(); + emit IERC20.Transfer(_alice, _charlie, accruedYield_ - fee_); + } + assertEq(_wrappedMToken.claimFor(_alice), accruedYield_); assertEq( @@ -824,7 +955,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false, false); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); vm.prank(_alice); @@ -891,7 +1022,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalNonEarningSupply(500); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false, false); _wrappedMToken.setAccountOf(_bob, 500); vm.expectEmit(); @@ -934,7 +1065,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalNonEarningSupply(1_000); _wrappedMToken.setAccountOf(_alice, 1_000); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex, false); + _wrappedMToken.setAccountOf(_bob, 500, _currentIndex, false, false); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -959,8 +1090,8 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setPrincipalOfTotalEarningSupply(1_363); _wrappedMToken.setTotalEarningSupply(1_500); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false, false); + _wrappedMToken.setAccountOf(_bob, 500, _currentIndex, false, false); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -1004,7 +1135,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setPrincipalOfTotalEarningSupply(909); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex, false, false); _mToken.setCurrentIndex((_currentIndex * 5) / 3); // 1_833333447838 @@ -1043,7 +1174,7 @@ contract WrappedMTokenTests is Test { aliceBalance_ = uint240(bound(aliceBalance_, 0, _getMaxAmount(aliceIndex_) / 4)); if (aliceEarning_) { - _wrappedMToken.setAccountOf(_alice, aliceBalance_, aliceIndex_, false); + _wrappedMToken.setAccountOf(_alice, aliceBalance_, aliceIndex_, false, false); _wrappedMToken.setTotalEarningSupply(aliceBalance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -1058,7 +1189,7 @@ contract WrappedMTokenTests is Test { bobBalance_ = uint240(bound(bobBalance_, 0, _getMaxAmount(bobIndex_) / 4)); if (bobEarning_) { - _wrappedMToken.setAccountOf(_bob, bobBalance_, bobIndex_, false); + _wrappedMToken.setAccountOf(_bob, bobBalance_, bobIndex_, false, false); _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + bobBalance_); _wrappedMToken.setPrincipalOfTotalEarningSupply( @@ -1152,7 +1283,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); @@ -1167,7 +1298,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); @@ -1194,7 +1325,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, balance_); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); _mToken.setCurrentIndex(index_); @@ -1211,9 +1342,49 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), balance_); } + /* ============ startEarningFor batch ============ */ + function test_startEarningFor_batch_earningIsDisabled() external { + vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); + _wrappedMToken.startEarningFor(new address[](2)); + } + + function test_startEarningFor_batch_notApprovedEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + + _wrappedMToken.enableEarning(); + + 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 { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _earnerManager.setEarnerDetails(_alice, true, 0, address(0)); + _earnerManager.setEarnerDetails(_bob, true, 0, address(0)); + + _wrappedMToken.enableEarning(); + + 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); @@ -1227,7 +1398,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setPrincipalOfTotalEarningSupply(909); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 999, _currentIndex, false, false); vm.expectEmit(); emit IWrappedMToken.StoppedEarning(_alice); @@ -1252,7 +1423,7 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningSupply(balance_); - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false); + _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_, false, false); _mToken.setCurrentIndex(index_); @@ -1272,7 +1443,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)); @@ -1280,7 +1451,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); @@ -1288,7 +1459,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); @@ -1296,12 +1467,45 @@ 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 { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 0, _currentIndex, false, false); + _wrappedMToken.setAccountOf(_bob, 0, _currentIndex, 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))); @@ -1393,11 +1597,11 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); @@ -1405,7 +1609,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false, false); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } @@ -1434,11 +1638,11 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 550); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); @@ -1446,7 +1650,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 2_200); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false, false); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); } @@ -1475,11 +1679,11 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.accruedYieldOf(_alice), 50); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); @@ -1487,18 +1691,18 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 1_200); - _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false); + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex, false, false); assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); } /* ============ lastIndexOf ============ */ function test_lastIndexOf() external { - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.lastIndexOf(_alice), _EXP_SCALED_ONE); - _wrappedMToken.setAccountOf(_alice, 0, 2 * _EXP_SCALED_ONE, false); + _wrappedMToken.setAccountOf(_alice, 0, 2 * _EXP_SCALED_ONE, false, false); assertEq(_wrappedMToken.lastIndexOf(_alice), 2 * _EXP_SCALED_ONE); } @@ -1509,7 +1713,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)); } @@ -1554,7 +1758,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); @@ -1570,7 +1774,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( 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 0f1bf8c..b1609a8 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -70,3 +70,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 a85b766..cee1f6d 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_); @@ -32,12 +33,18 @@ contract WrappedMTokenHarness is WrappedMToken { _accounts[account_].lastIndex = 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_, + uint256 index_, + bool hasClaimRecipient_, + bool hasEarnerDetails_ + ) external { + _accounts[account_] = Account(true, uint240(balance_), uint128(index_), 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 { @@ -58,9 +65,19 @@ contract WrappedMTokenHarness is WrappedMToken { function getAccountOf( address account_ - ) external view returns (bool isEarning_, uint240 balance_, uint128 index_, bool hasClaimRecipient_) { + ) + external + view + returns (bool isEarning_, uint240 balance_, uint128 index_, bool hasClaimRecipient_, bool hasEarnerDetails_) + { Account storage account = _accounts[account_]; - return (account.isEarning, account.balance, account.lastIndex, account.hasClaimRecipient); + return ( + account.isEarning, + account.balance, + account.lastIndex, + account.hasClaimRecipient, + account.hasEarnerDetails + ); } function getInternalClaimRecipientOf(address account_) external view returns (address claimRecipient_) {