diff --git a/script/DeployBase.sol b/script/DeployBase.sol index a68ddb9..20bebab 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -43,15 +43,18 @@ contract DeployBase { // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. // Wrapped M Token Proxy constructor needs `wrappedMTokenImplementation_`. - earnerManagerImplementation_ = address(new EarnerManager(registrar_, earnerManagerMigrationAdmin_)); + earnerManagerImplementation_ = address(new EarnerManager(registrar_)); earnerManagerProxy_ = address(new Proxy(earnerManagerImplementation_)); wrappedMTokenImplementation_ = address( - new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, wrappedMMigrationAdmin_) + new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_) ); wrappedMTokenProxy_ = address(new Proxy(wrappedMTokenImplementation_)); + + EarnerManager(earnerManagerProxy_).initialize(earnerManagerMigrationAdmin_); + WrappedMToken(wrappedMTokenProxy_).initialize(wrappedMMigrationAdmin_); } /** @@ -88,15 +91,19 @@ contract DeployBase { // Wrapped M Token Implementation constructor needs `earnerManagerProxy_`. // Migrator needs `wrappedMTokenImplementation_` addresses. - earnerManagerImplementation_ = address(new EarnerManager(registrar_, earnerManagerMigrationAdmin_)); + earnerManagerImplementation_ = address(new EarnerManager(registrar_)); earnerManagerProxy_ = address(new Proxy(earnerManagerImplementation_)); wrappedMTokenImplementation_ = address( - new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_, wrappedMMigrationAdmin_) + new WrappedMToken(mToken_, registrar_, earnerManagerProxy_, excessDestination_) + ); + + wrappedMTokenMigrator_ = address( + new WrappedMTokenMigratorV1(wrappedMTokenImplementation_, earners_, wrappedMMigrationAdmin_) ); - wrappedMTokenMigrator_ = address(new WrappedMTokenMigratorV1(wrappedMTokenImplementation_, earners_)); + EarnerManager(earnerManagerProxy_).initialize(earnerManagerMigrationAdmin_); } /** diff --git a/src/EarnerManager.sol b/src/EarnerManager.sol index 3859a07..c76dae4 100644 --- a/src/EarnerManager.sol +++ b/src/EarnerManager.sol @@ -39,29 +39,49 @@ contract EarnerManager is IEarnerManager, Migratable { /// @inheritdoc IEarnerManager address public immutable registrar; + /// @dev Used to check if the contract is the implementation or a proxy. + address internal immutable _self = address(this); + + /// @inheritdoc IEarnerManager + address public migrationAdmin; + /// @inheritdoc IEarnerManager - address public immutable migrationAdmin; + address public pendingMigrationAdmin; /// @dev Mapping of account to earner details. mapping(address account => EarnerDetails earnerDetails) internal _earnerDetails; /* ============ Modifiers ============ */ + modifier onlyMigrationAdmin() { + _revertIfNotMigrationAdmin(); + _; + } + modifier onlyAdmin() { _revertIfNotAdmin(); _; } - /* ============ Constructor ============ */ + /* ============ Constructor and Initializer ============ */ /** * @dev Constructs the contract. - * @param registrar_ The address of a Registrar contract. - * @param migrationAdmin_ The address of a migration admin. + * @param registrar_ The address of a Registrar contract. */ - constructor(address registrar_, address migrationAdmin_) { + constructor(address registrar_) { if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); - if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); + } + + /** + * @dev Used by a proxy of this implementation to initialize its state. + * @param migrationAdmin_ The address of a migration admin. + */ + function initialize(address migrationAdmin_) external { + if (address(this) == _self) revert NotProxy(); + if (migrationAdmin != address(0)) revert AlreadyInitialized(); + + _setMigrationAdmin(migrationAdmin_); } /* ============ Interactive Functions ============ */ @@ -95,12 +115,24 @@ contract EarnerManager is IEarnerManager, Migratable { /* ============ Temporary Admin Migration ============ */ /// @inheritdoc IEarnerManager - function migrate(address migrator_) external { - if (msg.sender != migrationAdmin) revert UnauthorizedMigration(); - + function migrate(address migrator_) external onlyMigrationAdmin { _migrate(migrator_); } + /// @inheritdoc IEarnerManager + function setPendingMigrationAdmin(address migrationAdmin_) external onlyMigrationAdmin { + emit PendingMigrationAdminSet(pendingMigrationAdmin = migrationAdmin_); + } + + /// @inheritdoc IEarnerManager + function acceptMigrationAdmin() external { + if (msg.sender != pendingMigrationAdmin) revert NotPendingMigrationAdmin(); + + _setMigrationAdmin(msg.sender); + + delete pendingMigrationAdmin; + } + /* ============ View/Pure Functions ============ */ /// @inheritdoc IEarnerManager @@ -228,10 +260,13 @@ contract EarnerManager is IEarnerManager, Migratable { } /** - * @dev Reverts if the caller is not an admin. + * @dev Sets the migration admin to `migrationAdmin_`. + * @param migrationAdmin_ The address of the account to set as the migration admin. */ - function _revertIfNotAdmin() internal view { - if (!isAdmin(msg.sender)) revert NotAdmin(); + function _setMigrationAdmin(address migrationAdmin_) internal { + if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); + + emit MigrationAdminSet(migrationAdmin_); } /* ============ Internal View/Pure Functions ============ */ @@ -255,4 +290,16 @@ contract EarnerManager is IEarnerManager, Migratable { function _isValidAdmin(address admin_) internal view returns (bool isValidAdmin_) { return (admin_ != address(0)) && isAdmin(admin_); } + + /** + * @dev Reverts if the caller is not an admin. + */ + function _revertIfNotAdmin() internal view { + if (!isAdmin(msg.sender)) revert NotAdmin(); + } + + /// @dev Reverts if the caller is not the migration admin. + function _revertIfNotMigrationAdmin() internal view { + if (msg.sender != migrationAdmin) revert NotMigrationAdmin(); + } } diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index b3e1110..d80b7a7 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -71,9 +71,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken address public immutable earnerManager; - /// @inheritdoc IWrappedMToken - address public immutable migrationAdmin; - /// @inheritdoc IWrappedMToken address public immutable mToken; @@ -83,11 +80,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken address public immutable excessDestination; - /// @inheritdoc IWrappedMToken - uint112 public totalEarningPrincipal; + /// @dev Used to check if the contract is the implementation or a proxy. + address internal immutable _self = address(this); /// @inheritdoc IWrappedMToken - int144 public roundingError; + uint112 public totalEarningPrincipal; /// @inheritdoc IWrappedMToken uint240 public totalEarningSupply; @@ -103,29 +100,49 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { mapping(address account => address claimRecipient) internal _claimRecipients; - /* ============ Constructor ============ */ + /// @inheritdoc IWrappedMToken + address public migrationAdmin; + + /// @inheritdoc IWrappedMToken + address public pendingMigrationAdmin; + + /* ============ Modifiers ============ */ + + modifier onlyMigrationAdmin() { + _revertIfNotMigrationAdmin(); + _; + } + + /* ============ Constructor and Initializer ============ */ /** * @dev Constructs the contract given an M Token address and migration admin. - * Note that a proxy will not need to initialize since there are no mutable storage values affected. * @param mToken_ The address of an M Token. * @param registrar_ The address of a Registrar. * @param 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_ + address excessDestination_ ) 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(); + } + + /** + * @dev Used by a proxy of this implementation to initialize its state. + * @param migrationAdmin_ The address of a migration admin. + */ + function initialize(address migrationAdmin_) external { + if (address(this) == _self) revert NotProxy(); + if (migrationAdmin != address(0)) revert AlreadyInitialized(); + + _setMigrationAdmin(migrationAdmin_); } /* ============ Interactive Functions ============ */ @@ -187,9 +204,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (excess_ <= 0) revert NoExcess(); - claimed_ = _getSafeTransferableM(address(this), uint240(uint248(excess_))); - - emit ExcessClaimed(claimed_); + emit ExcessClaimed(claimed_ = uint240(uint248(excess_))); // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. IMTokenLike(mToken).transfer(excessDestination, claimed_); @@ -271,12 +286,24 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /* ============ Temporary Admin Migration ============ */ /// @inheritdoc IWrappedMToken - function migrate(address migrator_) external { - if (msg.sender != migrationAdmin) revert UnauthorizedMigration(); - + function migrate(address migrator_) external onlyMigrationAdmin { _migrate(migrator_); } + /// @inheritdoc IWrappedMToken + function setPendingMigrationAdmin(address migrationAdmin_) external onlyMigrationAdmin { + emit PendingMigrationAdminSet(pendingMigrationAdmin = migrationAdmin_); + } + + /// @inheritdoc IWrappedMToken + function acceptMigrationAdmin() external { + if (msg.sender != pendingMigrationAdmin) revert NotPendingMigrationAdmin(); + + _setMigrationAdmin(msg.sender); + + delete pendingMigrationAdmin; + } + /* ============ View/Pure Functions ============ */ /// @inheritdoc IWrappedMToken @@ -342,11 +369,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function excess() public view returns (int248 excess_) { unchecked { - int248 earmarked_ = int248(uint248(totalNonEarningSupply + projectedEarningSupply())) + roundingError; - int248 balance_ = int248(uint248(_mBalanceOf(address(this)))); + uint240 earmarked_ = totalNonEarningSupply + projectedEarningSupply(); + uint240 balance_ = _mBalanceOf(address(this)); - // The entire M balance is excess if the total projected supply (factoring rounding errors) is less than 0. - return earmarked_ <= 0 ? balance_ : balance_ - earmarked_; + // The entire M balance is excess if the total projected supply (factoring rounding errors) is 0. + return + earmarked_ == 0 ? int248(uint248(balance_)) : int248(uint248(balance_)) - int248(uint248(earmarked_)); } } @@ -694,7 +722,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @return wrapped_ The amount of wM minted. */ function _wrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 wrapped_) { - _transferFromM(account_, amount_); + // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. + IMTokenLike(mToken).transferFrom(account_, address(this), amount_); + _mint(recipient_, wrapped_ = amount_); } @@ -706,8 +736,10 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @return unwrapped_ The amount of M withdrawn. */ function _unwrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 unwrapped_) { - _burn(account_, amount_); - _transferM(recipient_, unwrapped_ = amount_); + _burn(account_, unwrapped_ = amount_); + + // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. + IMTokenLike(mToken).transfer(recipient_, amount_); } /** @@ -773,45 +805,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { } /** - * @dev Transfer `amount_` M to `recipient_`, tracking this contract's M balance rounding errors. - * @param recipient_ The account to transfer M to. - * @param amount_ The amount of M to transfer. + * @dev Sets the migration admin to `migrationAdmin_`. + * @param migrationAdmin_ The address of the account to set as the migration admin. */ - function _transferM(address recipient_, uint240 amount_) internal { - uint240 startingBalance_ = _mBalanceOf(address(this)); - - // NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored. - IMTokenLike(mToken).transfer(recipient_, amount_); - - // NOTE: When this WrappedMToken contract is earning, any amount of M sent from it is converted to a principal - // amount at the MToken contract, which when represented as a present amount, may be a rounding error - // amount more than `amount_`. In order to capture the real decrease in M, the difference between the - // ending and starting M balance is captured. - uint240 decrease_ = startingBalance_ - _mBalanceOf(address(this)); - - // If the M lost is more than the wM burned, then the difference is added to `roundingError`. - roundingError += int144(int256(uint256(decrease_)) - int256(uint256(amount_))); - } - - /** - * @dev Transfer `amount_` M from `sender_`, tracking this contract's M balance rounding errors. - * @param sender_ The account to transfer M from. - * @param amount_ The amount of M to transfer. - */ - function _transferFromM(address sender_, uint240 amount_) internal { - uint240 startingBalance_ = _mBalanceOf(address(this)); - - // NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored. - IMTokenLike(mToken).transferFrom(sender_, address(this), _getSafeTransferableM(sender_, amount_)); - - // NOTE: When this WrappedMToken contract is earning, any amount of M sent to it is converted to a principal - // amount at the MToken contract, which when represented as a present amount, may be a rounding error - // amount more/less than `amount_`. In order to capture the real increase in M, the difference between the - // starting and ending M balance is captured. - uint240 increase_ = _mBalanceOf(address(this)) - startingBalance_; + function _setMigrationAdmin(address migrationAdmin_) internal { + if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin(); - // If the M gained is more/less than the wM minted, then the difference is subtracted/added to `roundingError`. - roundingError += int144(int256(uint256(amount_)) - int256(uint256(increase_))); + emit MigrationAdminSet(migrationAdmin_); } /* ============ Internal View/Pure Functions ============ */ @@ -874,30 +874,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return IRegistrarLike(registrar).get(key_); } - /** - * @dev Compute the adjusted amount of M that can safely be transferred out given the current index. - * @param amount_ Some amount to be transferred out of this contract. - * @return safeAmount_ The adjusted amount that can safely be transferred out. - */ - function _getSafeTransferableM(address sender_, uint240 amount_) internal view returns (uint240 safeAmount_) { - // If `sender` is not earning, no need to adjust `amount_`. - if (!IMTokenLike(mToken).isEarning(sender_)) return amount_; - - uint128 currentIndex_ = _currentMIndex(); - uint112 startingPrincipal_ = uint112(IMTokenLike(mToken).principalBalanceOf(sender_)); - uint240 startingBalance_ = IndexingMath.getPresentAmountRoundedDown(startingPrincipal_, currentIndex_); - - // Adjust `amount_` to ensure it's M balance decrement is limited to `amount_`. - unchecked { - uint112 minEndingPrincipal_ = IndexingMath.getPrincipalAmountRoundedUp( - startingBalance_ - amount_, - currentIndex_ - ); - - return IndexingMath.getPresentAmountRoundedDown(startingPrincipal_ - minEndingPrincipal_, currentIndex_); - } - } - /// @dev Returns the address of the contract to use as a migrator, if any. function _getMigrator() internal view override returns (address migrator_) { return @@ -935,6 +911,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (account_ == address(0)) revert InvalidRecipient(account_); } + /// @dev Reverts if the caller is not the migration admin. + function _revertIfNotMigrationAdmin() internal view { + if (msg.sender != migrationAdmin) revert NotMigrationAdmin(); + } + /** * @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/WrappedMTokenMigratorV1.sol b/src/WrappedMTokenMigratorV1.sol index 4dd3685..283c72b 100644 --- a/src/WrappedMTokenMigratorV1.sol +++ b/src/WrappedMTokenMigratorV1.sol @@ -48,15 +48,21 @@ contract WrappedMTokenMigratorV1 { address public immutable listOfEarnerToMigrate; - constructor(address newImplementation_, address[] memory earners_) { - newImplementation = newImplementation_; + address public immutable migrationAdmin; + + constructor(address newImplementation_, address[] memory earners_, address migrationAdmin_) { + if ((newImplementation = newImplementation_) == address(0)) revert(); listOfEarnerToMigrate = address(new ListOfEarnersToMigrate(earners_)); + + if ((migrationAdmin = migrationAdmin_) == address(0)) revert(); } fallback() external virtual { _migrateEarners(); + _setMigrationAdmin(migrationAdmin); + address newImplementation_ = newImplementation; assembly { @@ -98,4 +104,14 @@ contract WrappedMTokenMigratorV1 { accounts_.slot := 6 // `_accounts` is slot 6 in v1. } } + + /** + * @dev Sets the `migrationAdmin` slot to `migrationAdmin_`. + * @param migrationAdmin_ The address of the account to set the `migrationAdmin_` to. + */ + function _setMigrationAdmin(address migrationAdmin_) internal { + assembly { + sstore(9, migrationAdmin_) // `migrationAdmin` is slot 9 in v2. + } + } } diff --git a/src/interfaces/IEarnerManager.sol b/src/interfaces/IEarnerManager.sol index 1a02fa2..88ce6c8 100644 --- a/src/interfaces/IEarnerManager.sol +++ b/src/interfaces/IEarnerManager.sol @@ -20,8 +20,23 @@ interface IEarnerManager is IMigratable { */ event EarnerDetailsSet(address indexed account, bool indexed status, address indexed admin, uint16 feeRate); + /** + * @notice Emitted when the migration admin is set. + * @param migrationAdmin The address of the migration admin. + */ + event MigrationAdminSet(address indexed migrationAdmin); + + /** + * @notice Emitted when the pending migration admin is set. + * @param pendingMigrationAdmin The address of the migration admin that can accept the role. + */ + event PendingMigrationAdminSet(address indexed pendingMigrationAdmin); + /* ============ Custom Errors ============ */ + /// @notice Emitted when trying to initialize the contract (proxy) when it is already initialized. + error AlreadyInitialized(); + /// @notice Emitted when `account` is already in the earners list, so it cannot be added by an admin. error AlreadyInRegistrarEarnersList(address account); @@ -46,8 +61,14 @@ interface IEarnerManager is IMigratable { /// @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 the execution context is the implementation itself, rather than a proxy. + error NotProxy(); + + /// @notice Emitted when a restricted function is called by an account other than the migration admin. + error NotMigrationAdmin(); + + /// @notice Emitted when an account other than the pending migration admin is accepting the migration admin role. + error NotPendingMigrationAdmin(); /// @notice Emitted when an account (whose status is being set) is 0x0. error ZeroAccount(); @@ -58,6 +79,14 @@ interface IEarnerManager is IMigratable { /// @notice Emitted in constructor if Registrar is 0x0. error ZeroRegistrar(); + /* ============ Initializer ============ */ + + /** + * @dev Initializes the contract with a migration admin. + * @param migrationAdmin_ The address of a migration admin. + */ + function initialize(address migrationAdmin_) external; + /* ============ Interactive Functions ============ */ /** @@ -92,6 +121,15 @@ interface IEarnerManager is IMigratable { */ function migrate(address migrator) external; + /** + * @notice Sets the pending migration admin that can then accept the role and become the migration admin. + * @param migrationAdmin The address of an account to become the migration admin. + */ + function setPendingMigrationAdmin(address migrationAdmin) external; + + /// @notice Accepts the role of migration admin if the caller is the pending migration admin. + function acceptMigrationAdmin() external; + /* ============ View/Pure Functions ============ */ /// @notice Maximum fee rate that can be set (100% in basis points). @@ -173,6 +211,9 @@ interface IEarnerManager is IMigratable { /// @notice The account that can bypass the Registrar and call the `migrate(address migrator)` function. function migrationAdmin() external view returns (address migrationAdmin); + /// @notice The account that can accept the migration admin role. + function pendingMigrationAdmin() external view returns (address pendingMigrationAdmin); + /// @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 b3af5a3..0e969cc 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -45,6 +45,18 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ event ExcessClaimed(uint240 excess); + /** + * @notice Emitted when the migration admin is set. + * @param migrationAdmin The address of the migration admin. + */ + event MigrationAdminSet(address indexed migrationAdmin); + + /** + * @notice Emitted when the pending migration admin is set. + * @param pendingMigrationAdmin The address of the migration admin that can accept the role. + */ + event PendingMigrationAdminSet(address indexed pendingMigrationAdmin); + /** * @notice Emitted when `account` starts being an wM earner. * @param account The account that started earning. @@ -59,6 +71,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /* ============ Custom Errors ============ */ + /// @notice Emitted when trying to initialize the contract (proxy) when it is already initialized. + error AlreadyInitialized(); + /// @notice Emitted when performing an operation that is not allowed when earning is disabled. error EarningIsDisabled(); @@ -91,8 +106,14 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted when there is no excess to claim. error NoExcess(); - /// @notice Emitted when the non-governance migrate function is called by an account other than the migration admin. - error UnauthorizedMigration(); + /// @notice Emitted when the execution context is the implementation itself, rather than a proxy. + error NotProxy(); + + /// @notice Emitted when a restricted function is called by an account other than the migration admin. + error NotMigrationAdmin(); + + /// @notice Emitted when an account other than the pending migration admin is accepting the migration admin role. + error NotPendingMigrationAdmin(); /// @notice Emitted in constructor if Earner Manager is 0x0. error ZeroEarnerManager(); @@ -109,6 +130,14 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted in constructor if Registrar is 0x0. error ZeroRegistrar(); + /* ============ Initializer ============ */ + + /** + * @dev Initializes the contract with a migration admin. + * @param migrationAdmin_ The address of a migration admin. + */ + function initialize(address migrationAdmin_) external; + /* ============ Interactive Functions ============ */ /** @@ -232,6 +261,15 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ function migrate(address migrator) external; + /** + * @notice Sets the pending migration admin that can then accept the role and become the migration admin. + * @param migrationAdmin The address of an account to become the migration admin. + */ + function setPendingMigrationAdmin(address migrationAdmin) external; + + /// @notice Accepts the role of migration admin if the caller is the pending migration admin. + function acceptMigrationAdmin() external; + /* ============ View/Pure Functions ============ */ /// @notice 100% in basis points. @@ -302,6 +340,9 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the M Token contract. function mToken() external view returns (address mToken); + /// @notice The account that can accept the migration admin role. + function pendingMigrationAdmin() external view returns (address pendingMigrationAdmin); + /// @notice The projected total earning supply if all accrued yield was claimed at this moment. function projectedEarningSupply() external view returns (uint240 supply); @@ -325,7 +366,4 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The address of the destination where excess is claimed to. function excessDestination() external view returns (address excessDestination); - - /// @notice The amount of rounding error the contract has lost due to rounding in favour of users. - function roundingError() external view returns (int144 roundingError); } diff --git a/test/integration/Deploy.t.sol b/test/integration/Deploy.t.sol index d8b3d57..593f23a 100644 --- a/test/integration/Deploy.t.sol +++ b/test/integration/Deploy.t.sol @@ -51,12 +51,12 @@ contract DeployTests is Test, DeployBase { // Earner Manager Proxy assertions assertEq(earnerManagerProxy_, expectedEarnerManagerProxy_); assertEq(IEarnerManager(earnerManagerProxy_).registrar(), _REGISTRAR); + assertEq(IEarnerManager(earnerManagerProxy_).migrationAdmin(), _EARNER_MANAGER_MIGRATION_ADMIN); assertEq(IEarnerManager(earnerManagerProxy_).implementation(), earnerManagerImplementation_); // Wrapped M Token Implementation assertions assertEq(wrappedMTokenImplementation_, expectedWrappedMTokenImplementation_); assertEq(IWrappedMToken(wrappedMTokenImplementation_).earnerManager(), earnerManagerProxy_); - assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); assertEq(IWrappedMToken(wrappedMTokenImplementation_).excessDestination(), _EXCESS_DESTINATION); diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index 93cb894..debe8ac 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -99,7 +99,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -175,7 +175,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 200_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -200,7 +200,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 150_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.excess(), _excess -= 1); assertGe( int256(_wrapperBalanceOfM), @@ -287,7 +287,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -425,7 +425,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 1); - assertEq(_wrappedMToken.excess(), _excess -= 1); + assertEq(_wrappedMToken.excess(), _excess); assertGe( int256(_wrapperBalanceOfM), @@ -557,7 +557,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _bobBalance); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.excess(), _excess -= 1); // Assert Bob (Earner) assertEq(_mToken.balanceOf(_bob), _bobBalance); @@ -594,7 +594,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= 2); + assertEq(_wrappedMToken.excess(), _excess -= 1); // Assert Dave (Non-Earner) assertEq(_mToken.balanceOf(_dave), _daveBalance); @@ -608,14 +608,15 @@ contract ProtocolIntegrationTests is TestBase { uint256 vaultStartingBalance_ = _mToken.balanceOf(_excessDestination); - assertEq(_wrappedMToken.claimExcess(), uint256(_excess - 1)); - assertEq(_mToken.balanceOf(_excessDestination), uint256(_excess - 1) + vaultStartingBalance_); + assertEq(_wrappedMToken.claimExcess(), uint256(_excess)); + + assertEq(_mToken.balanceOf(_excessDestination), uint256(_excess) + vaultStartingBalance_); // Assert Globals assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply); assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply); assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield); - assertEq(_wrappedMToken.excess(), _excess -= _excess); + assertEq(_wrappedMToken.excess(), _excess -= _excess + 1); assertGe( int256(_wrapperBalanceOfM), @@ -624,17 +625,19 @@ contract ProtocolIntegrationTests is TestBase { } function testFuzz_full(uint256 seed_) external { - // TODO: Reinstate to test post-migration for new version. vm.skip(true); + _wrappedMToken.claimExcess(); + for (uint256 index_; index_ < _accounts.length; ++index_) { _giveM(_accounts[index_], 100_000e6); } - for (uint256 index_; index_ < 1000; ++index_) { - assertTrue(Invariants.checkInvariant1(address(_wrappedMToken), _accounts), "Invariant 1 Failed."); - assertTrue(Invariants.checkInvariant2(address(_wrappedMToken), _accounts), "Invariant 2 Failed."); - assertTrue(Invariants.checkInvariant4(address(_wrappedMToken), _accounts), "Invariant 4 Failed."); + for (uint256 index_; index_ < 1_000; ++index_) { + // assertTrue(Invariants.checkInvariant1(address(_wrappedMToken), _accounts), "Invariant 1 Failed."); + // assertTrue(Invariants.checkInvariant2(address(_wrappedMToken), _accounts), "Invariant 2 Failed."); + assertTrue(Invariants.checkInvariant3(address(_wrappedMToken), address(_mToken)), "Invariant 3 Failed."); + // assertTrue(Invariants.checkInvariant4(address(_wrappedMToken), _accounts), "Invariant 4 Failed."); // console2.log("--------"); // console2.log(""); @@ -648,12 +651,10 @@ contract ProtocolIntegrationTests is TestBase { // console2.log(""); // console2.log("--------"); - assertTrue(Invariants.checkInvariant1(address(_wrappedMToken), _accounts), "Invariant 1 Failed."); - assertTrue(Invariants.checkInvariant2(address(_wrappedMToken), _accounts), "Invariant 2 Failed."); - assertTrue(Invariants.checkInvariant4(address(_wrappedMToken), _accounts), "Invariant 4 Failed."); - - // NOTE: Skipping this as there is no trivial way to guarantee this invariant while meeting 1 and 2. - // assertTrue(Invariants.checkInvariant3(address(_wrappedMToken), address(_mToken)), "Invariant 3 Failed."); + // assertTrue(Invariants.checkInvariant1(address(_wrappedMToken), _accounts), "Invariant 1 Failed."); + // assertTrue(Invariants.checkInvariant2(address(_wrappedMToken), _accounts), "Invariant 2 Failed."); + assertTrue(Invariants.checkInvariant3(address(_wrappedMToken), address(_mToken)), "Invariant 3 Failed."); + // assertTrue(Invariants.checkInvariant4(address(_wrappedMToken), _accounts), "Invariant 4 Failed."); // console2.log("Wrapper has %s M", _mToken.balanceOf(address(_wrappedMToken))); diff --git a/test/integration/TestBase.sol b/test/integration/TestBase.sol index 3697d4c..53f3ff7 100644 --- a/test/integration/TestBase.sol +++ b/test/integration/TestBase.sol @@ -217,10 +217,13 @@ contract TestBase is Test { } function _deployV2Components() internal { - _earnerManagerImplementation = address(new EarnerManager(_registrar, _migrationAdmin)); + _earnerManagerImplementation = address(new EarnerManager(_registrar)); _earnerManager = address(new Proxy(_earnerManagerImplementation)); + + EarnerManager(_earnerManager).initialize(_migrationAdmin); + _wrappedMTokenImplementationV2 = address( - new WrappedMToken(address(_mToken), _registrar, _earnerManager, _excessDestination, _migrationAdmin) + new WrappedMToken(address(_mToken), _registrar, _earnerManager, _excessDestination) ); address[] memory earners_ = new address[](_earners.length); @@ -229,7 +232,9 @@ contract TestBase is Test { earners_[index_] = _earners[index_]; } - _wrappedMTokenMigratorV1 = address(new WrappedMTokenMigratorV1(_wrappedMTokenImplementationV2, earners_)); + _wrappedMTokenMigratorV1 = address( + new WrappedMTokenMigratorV1(_wrappedMTokenImplementationV2, earners_, _migrationAdmin) + ); } function _migrate() internal { diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol index 7f5a663..484c3eb 100644 --- a/test/integration/Upgrade.t.sol +++ b/test/integration/Upgrade.t.sol @@ -97,12 +97,12 @@ contract UpgradeTests is Test, DeployBase { // Earner Manager Proxy assertions assertEq(earnerManagerProxy_, expectedEarnerManagerProxy_); assertEq(IEarnerManager(earnerManagerProxy_).registrar(), _REGISTRAR); + assertEq(IEarnerManager(earnerManagerProxy_).migrationAdmin(), _EARNER_MANAGER_MIGRATION_ADMIN); assertEq(IEarnerManager(earnerManagerProxy_).implementation(), earnerManagerImplementation_); // Wrapped M Token Implementation assertions assertEq(wrappedMTokenImplementation_, expectedWrappedMTokenImplementation_); assertEq(IWrappedMToken(wrappedMTokenImplementation_).earnerManager(), earnerManagerProxy_); - assertEq(IWrappedMToken(wrappedMTokenImplementation_).migrationAdmin(), _WRAPPED_M_MIGRATION_ADMIN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(wrappedMTokenImplementation_).registrar(), _REGISTRAR); assertEq(IWrappedMToken(wrappedMTokenImplementation_).excessDestination(), _EXCESS_DESTINATION); @@ -111,6 +111,11 @@ contract UpgradeTests is Test, DeployBase { assertEq(wrappedMTokenMigrator_, expectedWrappedMTokenMigrator_); uint240 totalEarningSupply_ = IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(); + uint256[] memory balancesWithYield_ = new uint256[](_earners.length); + + for (uint256 index_; index_ < _earners.length; ++index_) { + balancesWithYield_[index_] = IWrappedMToken(_WRAPPED_M_TOKEN).balanceWithYieldOf(_earners[index_]); + } vm.prank(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin()); IWrappedMToken(_WRAPPED_M_TOKEN).migrate(wrappedMTokenMigrator_); @@ -125,6 +130,9 @@ contract UpgradeTests is Test, DeployBase { // Relevant storage slots. assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).totalEarningSupply(), totalEarningSupply_); - assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).roundingError(), 0); + + for (uint256 index_; index_ < _earners.length; ++index_) { + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).balanceWithYieldOf(_earners[index_]), balancesWithYield_[index_]); + } } } diff --git a/test/unit/EarnerManager.sol b/test/unit/EarnerManager.sol index 39c3e7d..5520bb3 100644 --- a/test/unit/EarnerManager.sol +++ b/test/unit/EarnerManager.sol @@ -2,6 +2,7 @@ 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"; @@ -23,14 +24,14 @@ contract EarnerManagerTests is Test { address internal _dave = makeAddr("dave"); address internal _frank = makeAddr("frank"); - address internal _migrationAdmin = makeAddr("migrationAdmin"); - MockRegistrar internal _registrar; + EarnerManagerHarness internal _implementation; EarnerManagerHarness internal _earnerManager; function setUp() external { _registrar = new MockRegistrar(); - _earnerManager = new EarnerManagerHarness(address(_registrar), _migrationAdmin); + _implementation = new EarnerManagerHarness(address(_registrar)); + _earnerManager = EarnerManagerHarness(address(new Proxy(address(_implementation)))); _registrar.setListContains(_ADMINS_LIST_NAME, _admin1, true); _registrar.setListContains(_ADMINS_LIST_NAME, _admin2, true); @@ -44,12 +45,32 @@ contract EarnerManagerTests is Test { /* ============ constructor ============ */ function test_constructor_zeroRegistrar() external { vm.expectRevert(IEarnerManager.ZeroRegistrar.selector); - new EarnerManagerHarness(address(0), address(0)); + new EarnerManagerHarness(address(0)); + } + + /* ============ initialize ============ */ + function test_initialize_notProxy() external { + vm.expectRevert(IEarnerManager.NotProxy.selector); + _implementation.initialize(address(0)); + } + + function test_initialize_alreadyInitialized() external { + _earnerManager.setMigrationAdmin(address(1)); + + vm.expectRevert(IEarnerManager.AlreadyInitialized.selector); + _earnerManager.initialize(address(0)); } - function test_constructor_zeroMigrationAdmin() external { + function test_initialize_zeroMigrationAdmin() external { vm.expectRevert(IEarnerManager.ZeroMigrationAdmin.selector); - new EarnerManagerHarness(address(_registrar), address(0)); + _earnerManager.initialize(address(0)); + } + + function test_initialize() external { + vm.expectEmit(); + emit IEarnerManager.MigrationAdminSet(address(1)); + + _earnerManager.initialize(address(1)); } /* ============ _setDetails ============ */ @@ -595,4 +616,46 @@ contract EarnerManagerTests is Test { assertTrue(_earnerManager.isAdmin(_admin1)); assertTrue(_earnerManager.isAdmin(_admin2)); } + + /* ============ setPendingMigrationAdmin ============ */ + function test_setPendingMigrationAdmin_notMigrationAdmin() external { + vm.expectRevert(IEarnerManager.NotMigrationAdmin.selector); + + vm.prank(_alice); + _earnerManager.setPendingMigrationAdmin(address(0)); + } + + function test_setPendingMigrationAdmin() external { + _earnerManager.setMigrationAdmin(_alice); + + vm.expectEmit(); + emit IEarnerManager.PendingMigrationAdminSet(_bob); + + vm.prank(_alice); + _earnerManager.setPendingMigrationAdmin(_bob); + + assertEq(_earnerManager.pendingMigrationAdmin(), _bob); + } + + /* ============ acceptMigrationAdmin ============ */ + function test_acceptMigrationAdmin_notPendingMigrationAdmin() external { + vm.expectRevert(IEarnerManager.NotPendingMigrationAdmin.selector); + + vm.prank(_bob); + _earnerManager.acceptMigrationAdmin(); + } + + function test_acceptMigrationAdmin() external { + _earnerManager.setMigrationAdmin(_alice); + _earnerManager.setInternalPendingMigrationAdmin(_bob); + + vm.expectEmit(); + emit IEarnerManager.MigrationAdminSet(_bob); + + vm.prank(_bob); + _earnerManager.acceptMigrationAdmin(); + + assertEq(_earnerManager.migrationAdmin(), _bob); + assertEq(_earnerManager.pendingMigrationAdmin(), address(0)); + } } diff --git a/test/unit/Migrations.t.sol b/test/unit/Migrations.t.sol index b58c89e..0677280 100644 --- a/test/unit/Migrations.t.sol +++ b/test/unit/Migrations.t.sol @@ -57,17 +57,11 @@ contract MigrationTests is Test { address mToken_ = makeAddr("mToken"); address implementation_ = address( - new WrappedMToken( - address(mToken_), - address(registrar_), - _earnerManager, - _excessDestination, - _migrationAdmin - ) + new WrappedMToken(address(mToken_), address(registrar_), _earnerManager, _excessDestination) ); address proxy_ = address(new Proxy(address(implementation_))); - address migrator_ = address(new WrappedMTokenMigrator(address(new Foo()), new address[](0))); + address migrator_ = address(new WrappedMTokenMigrator(address(new Foo()), new address[](0), _migrationAdmin)); registrar_.set(keccak256(abi.encode(_WM_MIGRATOR_KEY_PREFIX, proxy_)), bytes32(uint256(uint160(migrator_)))); @@ -84,17 +78,14 @@ contract MigrationTests is Test { address mToken_ = makeAddr("mToken"); address implementation_ = address( - new WrappedMToken( - address(mToken_), - address(registrar_), - _earnerManager, - _excessDestination, - _migrationAdmin - ) + new WrappedMToken(address(mToken_), address(registrar_), _earnerManager, _excessDestination) ); address proxy_ = address(new Proxy(address(implementation_))); - address migrator_ = address(new WrappedMTokenMigrator(address(new Foo()), new address[](0))); + + WrappedMToken(proxy_).initialize(_migrationAdmin); + + address migrator_ = address(new WrappedMTokenMigrator(address(new Foo()), new address[](0), _migrationAdmin)); vm.expectRevert(); Foo(proxy_).bar(); @@ -108,7 +99,7 @@ contract MigrationTests is Test { function test_earnerManager_migration() external { MockRegistrar registrar_ = new MockRegistrar(); - address implementation_ = address(new EarnerManager(address(registrar_), _migrationAdmin)); + address implementation_ = address(new EarnerManager(address(registrar_))); address proxy_ = address(new Proxy(address(implementation_))); address migrator_ = address(new EarnerManagerMigrator(address(new Foo()))); @@ -117,7 +108,7 @@ contract MigrationTests is Test { vm.expectRevert(); Foo(proxy_).bar(); - IWrappedMToken(proxy_).migrate(); + IEarnerManager(proxy_).migrate(); assertEq(Foo(proxy_).bar(), 1); } @@ -125,8 +116,11 @@ contract MigrationTests is Test { function test_earnerManager_migration_fromAdmin() external { MockRegistrar registrar_ = new MockRegistrar(); - address implementation_ = address(new EarnerManager(address(registrar_), _migrationAdmin)); + address implementation_ = address(new EarnerManager(address(registrar_))); address proxy_ = address(new Proxy(address(implementation_))); + + EarnerManager(proxy_).initialize(_migrationAdmin); + address migrator_ = address(new EarnerManagerMigrator(address(new Foo()))); vm.expectRevert(); diff --git a/test/unit/Stories.t.sol b/test/unit/Stories.t.sol index 75d4a63..fad24e8 100644 --- a/test/unit/Stories.t.sol +++ b/test/unit/Stories.t.sol @@ -24,7 +24,6 @@ contract StoryTests is Test { address internal _dave = makeAddr("dave"); address internal _excessDestination = makeAddr("excessDestination"); - address internal _migrationAdmin = makeAddr("migrationAdmin"); MockEarnerManager internal _earnerManager; MockM internal _mToken; @@ -44,8 +43,7 @@ contract StoryTests is Test { address(_mToken), address(_registrar), address(_earnerManager), - _excessDestination, - _migrationAdmin + _excessDestination ); _wrappedMToken = IWrappedMToken(address(new Proxy(address(_implementation)))); diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 44c3603..661440e 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -34,7 +34,6 @@ contract WrappedMTokenTests is Test { address internal _david = makeAddr("david"); address internal _excessDestination = makeAddr("excessDestination"); - address internal _migrationAdmin = makeAddr("migrationAdmin"); address[] internal _accounts = [_alice, _bob, _charlie, _david]; @@ -55,8 +54,7 @@ contract WrappedMTokenTests is Test { address(_mToken), address(_registrar), address(_earnerManager), - _excessDestination, - _migrationAdmin + _excessDestination ); _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); @@ -72,7 +70,6 @@ contract WrappedMTokenTests is Test { /* ============ constructor ============ */ function test_constructor() external view { - assertEq(_wrappedMToken.migrationAdmin(), _migrationAdmin); assertEq(_wrappedMToken.mToken(), address(_mToken)); assertEq(_wrappedMToken.registrar(), address(_registrar)); assertEq(_wrappedMToken.excessDestination(), _excessDestination); @@ -84,39 +81,22 @@ contract WrappedMTokenTests is Test { function test_constructor_zeroMToken() external { vm.expectRevert(IWrappedMToken.ZeroMToken.selector); - new WrappedMTokenHarness(address(0), address(0), address(0), address(0), address(0)); + new WrappedMTokenHarness(address(0), address(0), address(0), address(0)); } function test_constructor_zeroRegistrar() external { vm.expectRevert(IWrappedMToken.ZeroRegistrar.selector); - new WrappedMTokenHarness(address(_mToken), address(0), address(0), address(0), address(0)); + new WrappedMTokenHarness(address(_mToken), address(0), address(0), address(0)); } function test_constructor_zeroEarnerManager() external { vm.expectRevert(IWrappedMToken.ZeroEarnerManager.selector); - new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0), address(0)); + new WrappedMTokenHarness(address(_mToken), address(_registrar), address(0), address(0)); } function test_constructor_zeroExcessDestination() external { vm.expectRevert(IWrappedMToken.ZeroExcessDestination.selector); - 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), - address(_earnerManager), - _excessDestination, - address(0) - ); + new WrappedMTokenHarness(address(_mToken), address(_registrar), address(_earnerManager), address(0)); } function test_constructor_zeroImplementation() external { @@ -124,6 +104,31 @@ contract WrappedMTokenTests is Test { WrappedMTokenHarness(address(new Proxy(address(0)))); } + /* ============ initialize ============ */ + function test_initialize_notProxy() external { + vm.expectRevert(IWrappedMToken.NotProxy.selector); + _implementation.initialize(address(0)); + } + + function test_initialize_alreadyInitialized() external { + _wrappedMToken.setMigrationAdmin(address(1)); + + vm.expectRevert(IWrappedMToken.AlreadyInitialized.selector); + _wrappedMToken.initialize(address(0)); + } + + function test_initialize_zeroMigrationAdmin() external { + vm.expectRevert(IWrappedMToken.ZeroMigrationAdmin.selector); + _wrappedMToken.initialize(address(0)); + } + + function test_initialize() external { + vm.expectEmit(); + emit IWrappedMToken.MigrationAdminSet(address(1)); + + _wrappedMToken.initialize(address(1)); + } + /* ============ _wrap ============ */ function test_internalWrap_insufficientAmount() external { vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, 0)); @@ -906,8 +911,7 @@ contract WrappedMTokenTests is Test { uint128 currentMIndex_, uint240 totalNonEarningSupply_, uint240 totalProjectedEarningSupply_, - uint112 mPrincipalBalance_, - int144 roundingError_ + uint112 mPrincipalBalance_ ) external { currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); @@ -937,13 +941,10 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); - roundingError_ = int144(bound(roundingError_, -1_000_000000, 1_000_000000)); - - _wrappedMToken.setRoundingError(roundingError_); - - uint240 totalProjectedSupply_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; - int248 earmarked_ = int248(uint248(totalProjectedSupply_)) + roundingError_; - int248 excess_ = earmarked_ <= 0 ? int248(uint248(mBalance_)) : int248(uint248(mBalance_)) - earmarked_; + uint240 earmarked_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; + int248 excess_ = earmarked_ <= 0 + ? int248(uint248(mBalance_)) + : int248(uint248(mBalance_)) - int248(uint248(earmarked_)); if (excess_ <= 0) { vm.expectRevert(IWrappedMToken.NoExcess.selector); @@ -1869,41 +1870,21 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.excess(), 0); - _wrappedMToken.setRoundingError(1); - - assertEq(_wrappedMToken.excess(), -1); - _mToken.setBalanceOf(address(_wrappedMToken), 2_101); - assertEq(_wrappedMToken.excess(), 0); + assertEq(_wrappedMToken.excess(), 1); _mToken.setBalanceOf(address(_wrappedMToken), 2_102); - assertEq(_wrappedMToken.excess(), 1); + assertEq(_wrappedMToken.excess(), 2); _mToken.setBalanceOf(address(_wrappedMToken), 3_102); - assertEq(_wrappedMToken.excess(), 1_001); + assertEq(_wrappedMToken.excess(), 1_002); _mToken.setCurrentIndex(1_210000000000); - assertEq(_wrappedMToken.excess(), 891); - - _wrappedMToken.setRoundingError(0); - assertEq(_wrappedMToken.excess(), 892); - - _wrappedMToken.setRoundingError(-1); - - assertEq(_wrappedMToken.excess(), 893); - - _wrappedMToken.setRoundingError(-2_210); - - assertEq(_wrappedMToken.excess(), 3_102); - - _wrappedMToken.setRoundingError(-2_211); - - assertEq(_wrappedMToken.excess(), 3_102); } function testFuzz_excess( @@ -1911,8 +1892,7 @@ contract WrappedMTokenTests is Test { uint128 currentMIndex_, uint240 totalNonEarningSupply_, uint240 totalProjectedEarningSupply_, - uint112 mPrincipalBalance_, - int144 roundingError_ + uint112 mPrincipalBalance_ ) external { currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); @@ -1942,14 +1922,9 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setTotalEarningPrincipal(totalEarningPrincipal_); _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); - roundingError_ = int144(bound(roundingError_, -1_000_000000, 1_000_000000)); + uint240 earmarked_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; - _wrappedMToken.setRoundingError(roundingError_); - - uint240 totalProjectedSupply_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; - int248 earmarked_ = int248(uint248(totalProjectedSupply_)) + roundingError_; - - assertLe(_wrappedMToken.excess(), int248(uint248(mBalance_)) - earmarked_); + assertLe(_wrappedMToken.excess(), int248(uint248(mBalance_)) - int248(uint248(earmarked_))); } /* ============ totalAccruedYield ============ */ @@ -1975,6 +1950,48 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 310); } + /* ============ setPendingMigrationAdmin ============ */ + function test_setPendingMigrationAdmin_notMigrationAdmin() external { + vm.expectRevert(IWrappedMToken.NotMigrationAdmin.selector); + + vm.prank(_alice); + _wrappedMToken.setPendingMigrationAdmin(address(0)); + } + + function test_setPendingMigrationAdmin() external { + _wrappedMToken.setMigrationAdmin(_alice); + + vm.expectEmit(); + emit IWrappedMToken.PendingMigrationAdminSet(_bob); + + vm.prank(_alice); + _wrappedMToken.setPendingMigrationAdmin(_bob); + + assertEq(_wrappedMToken.pendingMigrationAdmin(), _bob); + } + + /* ============ acceptMigrationAdmin ============ */ + function test_acceptMigrationAdmin_notPendingMigrationAdmin() external { + vm.expectRevert(IWrappedMToken.NotPendingMigrationAdmin.selector); + + vm.prank(_bob); + _wrappedMToken.acceptMigrationAdmin(); + } + + function test_acceptMigrationAdmin() external { + _wrappedMToken.setMigrationAdmin(_alice); + _wrappedMToken.setInternalPendingMigrationAdmin(_bob); + + vm.expectEmit(); + emit IWrappedMToken.MigrationAdminSet(_bob); + + vm.prank(_bob); + _wrappedMToken.acceptMigrationAdmin(); + + assertEq(_wrappedMToken.migrationAdmin(), _bob); + assertEq(_wrappedMToken.pendingMigrationAdmin(), address(0)); + } + /* ============ utils ============ */ function _getPrincipalAmountRoundedDown(uint240 presentAmount_, uint128 index_) internal pure returns (uint112) { return IndexingMath.divide240By128Down(presentAmount_, index_); diff --git a/test/utils/EarnerManagerHarness.sol b/test/utils/EarnerManagerHarness.sol index fb2e798..23de7d9 100644 --- a/test/utils/EarnerManagerHarness.sol +++ b/test/utils/EarnerManagerHarness.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.26; import { EarnerManager } from "../../src/EarnerManager.sol"; contract EarnerManagerHarness is EarnerManager { - constructor(address registrar_, address migrationAdmin_) EarnerManager(registrar_, migrationAdmin_) {} + constructor(address registrar_) EarnerManager(registrar_) {} function setInternalEarnerDetails(address account_, address admin_, uint16 feeRate_) external { _earnerDetails[account_] = EarnerDetails(admin_, feeRate_); @@ -14,4 +14,12 @@ contract EarnerManagerHarness is EarnerManager { function setDetails(address account_, bool status_, uint16 feeRate_) external { _setDetails(account_, status_, feeRate_); } + + function setMigrationAdmin(address migrationAdmin_) external { + migrationAdmin = migrationAdmin_; + } + + function setInternalPendingMigrationAdmin(address pendingMigrationAdmin_) external { + pendingMigrationAdmin = pendingMigrationAdmin_; + } } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index dd426c9..9a2e052 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -9,9 +9,8 @@ contract WrappedMTokenHarness is WrappedMToken { address mToken_, address registrar_, address earnerManager_, - address excessDestination_, - address migrationAdmin_ - ) WrappedMToken(mToken_, registrar_, earnerManager_, excessDestination_, migrationAdmin_) {} + address excessDestination_ + ) WrappedMToken(mToken_, registrar_, earnerManager_, excessDestination_) {} function internalWrap(address account_, address recipient_, uint240 amount_) external returns (uint240 wrapped_) { return _wrap(account_, recipient_, amount_); @@ -73,14 +72,18 @@ contract WrappedMTokenHarness is WrappedMToken { _enableDisableEarningIndices.push(index_); } - function setRoundingError(int256 roundingError_) external { - roundingError = int144(roundingError_); - } - function setHasEarnerDetails(address account_, bool hasEarnerDetails_) external { _accounts[account_].hasEarnerDetails = hasEarnerDetails_; } + function setMigrationAdmin(address migrationAdmin_) external { + migrationAdmin = migrationAdmin_; + } + + function setInternalPendingMigrationAdmin(address pendingMigrationAdmin_) external { + pendingMigrationAdmin = pendingMigrationAdmin_; + } + function getAccountOf( address account_ )