diff --git a/src/MigratorV1.sol b/src/MigratorV1.sol index 74cef78..6773f8e 100644 --- a/src/MigratorV1.sol +++ b/src/MigratorV1.sol @@ -1,12 +1,14 @@ -// SPDX-License-Identifier: BUSL-1.1 - pragma solidity 0.8.26; +import { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol"; + /** * @title Migrator contract for migrating a WrappedMToken contract from V1 to V2. * @author M^0 Labs */ contract MigratorV1 { + error InvalidEnableDisableEarningIndicesArrayLength(); + /// @dev Storage slot with the address of the current factory. `keccak256('eip1967.proxy.implementation') - 1`. uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; @@ -17,10 +19,71 @@ contract MigratorV1 { } fallback() external virtual { + (bool earningEnabled_, uint128 disableIndex_) = _clearEnableDisableEarningIndices(); + + if (earningEnabled_) { + _setEnableMIndex(IndexingMath.EXP_SCALED_ONE); + } else { + _setDisableIndex(disableIndex_); + } + address newImplementation_ = newImplementation; assembly { sstore(_IMPLEMENTATION_SLOT, newImplementation_) } } + + /** + * @dev Clears the entire `_enableDisableEarningIndices` array in storage, returning useful information. + * @return earningEnabled_ Whether earning is enabled. + * @return disableIndex_ The index when earning was disabled, if any. + */ + function _clearEnableDisableEarningIndices() internal returns (bool earningEnabled_, uint128 disableIndex_) { + uint128[] storage array_; + + assembly { + array_.slot := 7 // `_enableDisableEarningIndices` was slot 7 in v1. + } + + // If the array is empty, earning is disabled and thus the disable index was non-existent. + if (array_.length == 0) return (false, 0); + + // If the array has one element, earning is enabled and the disable index is non-existent. + if (array_.length == 1) { + array_.pop(); + return (true, 0); + } + + // If the array has two elements, earning is disabled and the disable index is the second element. + if (array_.length == 2) { + disableIndex_ = array_[1]; + array_.pop(); + array_.pop(); + return (false, disableIndex_); + } + + // In v1, it is not possible for the `_enableDisableEarningIndices` array to have more than two elements. + revert InvalidEnableDisableEarningIndicesArrayLength(); + } + + /** + * @dev Sets the `enableMIndex` slot to `index_`. + * @param index_ The index to set the `enableMIndex . + */ + function _setEnableMIndex(uint128 index_) internal { + assembly { + sstore(7, index_) // `enableMIndex` is the lower half of slot 7 in v2. + } + } + + /** + * @dev Sets the `disableIndex` slot to `index_`. + * @param index_ The index to set the `disableIndex . + */ + function _setDisableIndex(uint128 index_) internal { + assembly { + sstore(7, shl(128, index_)) // `disableIndex` is the upper half of slot 7 in v2. + } + } } diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index ab090b8..ca945de 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -104,8 +104,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @dev Mapping of accounts to their respective `AccountInfo` structs. mapping(address account => Account balance) internal _accounts; - /// @dev Array of indices at which earning was enabled or disabled. - uint128[] internal _enableDisableEarningIndices; + /// @inheritdoc IWrappedMToken + uint128 public enableMIndex; + + /// @inheritdoc IWrappedMToken + uint128 public disableIndex; /* ============ Constructor ============ */ @@ -195,17 +198,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (isEarningEnabled()) revert EarningIsEnabled(); - // NOTE: This is a temporary measure to prevent re-enabling earning after it has been disabled. - // This line will be removed in the future. - if (wasEarningEnabled()) revert EarningCannotBeReenabled(); - - uint128 currentMIndex_ = _currentMIndex(); - - _enableDisableEarningIndices.push(currentMIndex_); + emit EarningEnabled(enableMIndex = _currentMIndex()); IMTokenLike(mToken).startEarning(); - - emit EarningEnabled(currentMIndex_); } /// @inheritdoc IWrappedMToken @@ -214,21 +209,16 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { if (!isEarningEnabled()) revert EarningIsDisabled(); - uint128 currentMIndex_ = _currentMIndex(); + emit EarningDisabled(disableIndex = currentIndex()); - _enableDisableEarningIndices.push(currentMIndex_); + delete enableMIndex; IMTokenLike(mToken).stopEarning(); - - emit EarningDisabled(currentMIndex_); } /// @inheritdoc IWrappedMToken function startEarningFor(address account_) external { - if (!isEarningEnabled()) revert EarningIsDisabled(); - - // NOTE: Use `currentIndex()` if/when upgrading to support `startEarningFor` while earning is disabled. - _startEarningFor(account_, _currentMIndex()); + _startEarningFor(account_, currentIndex()); } /// @inheritdoc IWrappedMToken @@ -301,9 +291,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function currentIndex() public view returns (uint128 index_) { - if (isEarningEnabled()) return _currentMIndex(); + uint128 disableIndex_ = disableIndex == 0 ? IndexingMath.EXP_SCALED_ONE : disableIndex; - return UIntMath.max128(IndexingMath.EXP_SCALED_ONE, _lastDisableEarningIndex()); + return enableMIndex == 0 ? disableIndex_ : (disableIndex_ * _currentMIndex()) / enableMIndex; } /// @inheritdoc IWrappedMToken @@ -313,12 +303,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function isEarningEnabled() public view returns (bool isEnabled_) { - return _enableDisableEarningIndices.length % 2 == 1; - } - - /// @inheritdoc IWrappedMToken - function wasEarningEnabled() public view returns (bool wasEarning_) { - return _enableDisableEarningIndices.length != 0; + return enableMIndex != 0; } /// @inheritdoc IWrappedMToken @@ -712,11 +697,6 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { return IMTokenLike(mToken).currentIndex(); } - /// @dev Returns the earning index from the last `disableEarning` call. - function _lastDisableEarningIndex() internal view returns (uint128 index_) { - return wasEarningEnabled() ? _unsafeAccess(_enableDisableEarningIndices, 1) : 0; - } - /** * @dev Compute the yield given an account's balance, earning principal, and the current index. * @param balance_ The token balance of an earning account. @@ -847,23 +827,4 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { 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. - * @param array_ The storage pointer of an array of uint128 values. - * @param i_ The index of the array to read. - */ - function _unsafeAccess(uint128[] storage array_, uint256 i_) internal view returns (uint128 value_) { - assembly { - mstore(0, array_.slot) - - value_ := sload(add(keccak256(0, 0x20), div(i_, 2))) - - // Since uint128 values take up either the top half or bottom half of a slot, shift the result accordingly. - if eq(mod(i_, 2), 1) { - value_ := shr(128, value_) - } - } - } } diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 414d5a6..addb891 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -22,13 +22,13 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /** * @notice Emitted when Wrapped M earning is enabled. - * @param index The index at the moment earning is enabled. + * @param index The M index at the moment earning is enabled. */ event EarningEnabled(uint128 index); /** * @notice Emitted when Wrapped M earning is disabled. - * @param index The index at the moment earning is disabled. + * @param index The WrappedM index at the moment earning is disabled. */ event EarningDisabled(uint128 index); @@ -61,9 +61,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted when performing an operation that is not allowed when earning is enabled. error EarningIsEnabled(); - /// @notice Emitted when trying to enable earning after it has been explicitly disabled. - error EarningCannotBeReenabled(); - /** * @notice Emitted when calling `stopEarning` for an account approved as an earner by the Registrar. * @param account The account that is an approved earner. @@ -261,9 +258,15 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice The current index of Wrapped M's earning mechanism. function currentIndex() external view returns (uint128 index); + /// @notice The M token's index when earning was most recently enabled. + function enableMIndex() external view returns (uint128 enableMIndex); + /// @notice This contract's current excess M that is not earmarked for account balances or accrued yield. function excess() external view returns (uint240 excess); + /// @notice The wrapper's index when earning was most recently disabled. + function disableIndex() external view returns (uint128 disableIndex); + /** * @notice Returns whether `account` is a wM earner. * @param account The account being queried. @@ -274,9 +277,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Whether Wrapped M earning is enabled. function isEarningEnabled() external view returns (bool isEnabled); - /// @notice Whether Wrapped M earning has been enabled at least once. - function wasEarningEnabled() external view returns (bool wasEnabled); - /// @notice The account that can bypass the Registrar and call the `migrate(address migrator)` function. function migrationAdmin() external view returns (address migrationAdmin); diff --git a/test/integration/Migration.sol b/test/integration/Migration.sol new file mode 100644 index 0000000..bad2eb6 --- /dev/null +++ b/test/integration/Migration.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +import { IndexingMath } from "../../lib/common/src/libs/IndexingMath.sol"; + +import { TestBase } from "./TestBase.sol"; + +contract MigrationIntegrationTests is TestBase { + function test_index_noMigration() external { + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.currentIndex(), 1_073787769981); + } + + function test_index_migrate_beforeEarningDisabled() external { + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 0); + assertEq(_wrappedMToken.enableMIndex(), IndexingMath.EXP_SCALED_ONE); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.currentIndex(), 1_073787769981); + } + + function test_index_migrate_afterEarningDisabled() external { + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + _removeFromList(_EARNERS_LIST_NAME, address(_wrappedMToken)); + + _wrappedMToken.disableEarning(); + + _deployV2Components(); + _migrate(); + + assertEq(_wrappedMToken.disableIndex(), 1_023463403719); + assertEq(_wrappedMToken.enableMIndex(), 0); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + + vm.warp(vm.getBlockTimestamp() + 365 days); + + assertEq(_wrappedMToken.currentIndex(), 1_023463403719); + } +} diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index 47a776f..1c9d20d 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -75,6 +75,8 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.symbol(), "wM"); assertEq(_wrappedMToken.decimals(), 6); assertEq(_wrappedMToken.implementation(), address(_implementation)); + assertEq(_wrappedMToken.enableMIndex(), 0); + assertEq(_wrappedMToken.disableIndex(), 0); } function test_constructor_zeroMToken() external { @@ -147,8 +149,8 @@ contract WrappedMTokenTests is Test { } function test_wrap_toEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _mToken.setBalanceOf(_alice, 1_002); @@ -220,11 +222,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -274,11 +282,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -326,11 +340,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -378,11 +398,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -431,8 +457,8 @@ contract WrappedMTokenTests is Test { } function test_internalUnwrap_insufficientBalance_fromEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 999, 909); @@ -442,8 +468,8 @@ contract WrappedMTokenTests is Test { function test_internalUnwrap_fromNonEarner() external { _mToken.setIsEarning(address(_wrappedMToken), true); - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _mToken.setBalanceOf(address(_wrappedMToken), 1_000); @@ -501,8 +527,8 @@ contract WrappedMTokenTests is Test { function test_internalUnwrap_fromEarner() external { _mToken.setIsEarning(address(_wrappedMToken), true); - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _mToken.setBalanceOf(address(_wrappedMToken), 1_000); @@ -574,11 +600,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint240 unwrapAmount_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -622,11 +654,17 @@ contract WrappedMTokenTests is Test { bool accountEarning_, uint240 balanceWithYield_, uint240 balance_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -666,8 +704,8 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); @@ -694,8 +732,8 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner_withOverrideRecipient() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _registrar.set( keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), @@ -739,11 +777,17 @@ contract WrappedMTokenTests is Test { uint240 balanceWithYield_, uint240 balance_, uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, bool claimOverride_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -779,13 +823,19 @@ contract WrappedMTokenTests is Test { function testFuzz_claimExcess( bool earningEnabled_, uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, uint240 totalNonEarningSupply_, uint240 projectedTotalEarningSupply_, uint240 mBalance_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); uint128 currentIndex_ = _wrappedMToken.currentIndex(); uint240 maxAmount_ = _getMaxAmount(currentIndex_); @@ -848,8 +898,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_insufficientBalance_fromEarner_toNonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 1_000, 1_000); // 1_100 balance with yield. @@ -909,8 +959,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromEarner_toNonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); @@ -958,8 +1008,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromNonEarner_toEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(500); _wrappedMToken.setTotalEarningSupply(500); @@ -990,8 +1040,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_fromEarner_toEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_500); _wrappedMToken.setTotalEarningSupply(1_500); @@ -1039,8 +1089,8 @@ contract WrappedMTokenTests is Test { } function test_transfer_earnerToSelf() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); @@ -1074,11 +1124,17 @@ contract WrappedMTokenTests is Test { uint240 bobBalanceWithYield_, uint240 bobBalance_, uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, uint240 amount_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (aliceBalanceWithYield_, aliceBalance_) = _getFuzzedBalances( aliceBalanceWithYield_, @@ -1129,23 +1185,16 @@ contract WrappedMTokenTests is Test { } /* ============ startEarningFor ============ */ - function test_startEarningFor_earningIsDisabled() external { - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.startEarningFor(_alice); - } - function test_startEarningFor_notApprovedEarner() external { - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, _alice)); _wrappedMToken.startEarningFor(_alice); } function test_startEarning_overflow() external { - _mToken.setCurrentIndex(1_000000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - uint240 aliceBalance_ = uint240(type(uint112).max) + 1; + uint240 aliceBalance_ = uint240(type(uint112).max) + 20; // TODO: _getMaxAmount(1_100000000000) + 2; ? _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); @@ -1158,8 +1207,8 @@ contract WrappedMTokenTests is Test { } function test_startEarningFor() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalNonEarningSupply(1_000); @@ -1181,30 +1230,34 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 1_000); } - function testFuzz_startEarningFor(bool earningEnabled_, uint240 balance_, uint128 currentMIndex_) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + function testFuzz_startEarningFor( + bool earningEnabled_, + uint240 balance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); uint128 currentIndex_ = _wrappedMToken.currentIndex(); balance_ = uint240(bound(balance_, 0, _getMaxAmount(currentIndex_))); - _setupAccount(_alice, false, balance_, balance_); + _setupAccount(_alice, false, 0, balance_); _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - if (earningEnabled_) { - vm.expectEmit(); - emit IWrappedMToken.StartedEarning(_alice); - } else { - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - } + vm.expectEmit(); + emit IWrappedMToken.StartedEarning(_alice); _wrappedMToken.startEarningFor(_alice); - if (!earningEnabled_) return; - uint112 earningPrincipal_ = IndexingMath.getPrincipalAmountRoundedDown(balance_, currentIndex_); assertEq(_wrappedMToken.isEarning(_alice), true); @@ -1225,8 +1278,8 @@ contract WrappedMTokenTests is Test { } function test_stopEarningFor() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(1_000); _wrappedMToken.setTotalEarningSupply(1_000); @@ -1261,11 +1314,17 @@ contract WrappedMTokenTests is Test { bool earningEnabled_, uint240 balanceWithYield_, uint240 balance_, - uint128 currentMIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); (balanceWithYield_, balance_) = _getFuzzedBalances( balanceWithYield_, @@ -1310,14 +1369,18 @@ contract WrappedMTokenTests is Test { function test_enableEarning() external { _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _mToken.setCurrentIndex(1_100000000000); + _mToken.setCurrentIndex(1_210000000000); + + assertEq(_wrappedMToken.enableMIndex(), 0); + assertEq(_wrappedMToken.currentIndex(), 1_000000000000); vm.expectEmit(); - emit IWrappedMToken.EarningEnabled(1_100000000000); + emit IWrappedMToken.EarningEnabled(1_210000000000); _wrappedMToken.enableEarning(); - assertEq(_wrappedMToken.currentIndex(), 1_100000000000); + assertEq(_wrappedMToken.enableMIndex(), 1_210000000000); + assertEq(_wrappedMToken.currentIndex(), 1_000000000000); } /* ============ disableEarning ============ */ @@ -1334,21 +1397,27 @@ contract WrappedMTokenTests is Test { } function test_disableEarning() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + assertEq(_wrappedMToken.enableMIndex(), 1_100000000000); + assertEq(_wrappedMToken.disableIndex(), 0); + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); vm.expectEmit(); emit IWrappedMToken.EarningDisabled(1_100000000000); _wrappedMToken.disableEarning(); + assertEq(_wrappedMToken.enableMIndex(), 0); + assertEq(_wrappedMToken.disableIndex(), 1_100000000000); assertEq(_wrappedMToken.currentIndex(), 1_100000000000); } /* ============ balanceOf ============ */ function test_balanceOf_nonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1364,8 +1433,8 @@ contract WrappedMTokenTests is Test { } function test_balanceOf_earner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500, 500); // 550 balance with yield. @@ -1390,8 +1459,8 @@ contract WrappedMTokenTests is Test { /* ============ balanceWithYieldOf ============ */ function test_balanceWithYieldOf_nonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1407,8 +1476,8 @@ contract WrappedMTokenTests is Test { } function test_balanceWithYieldOf_earner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500, 500); // 550 balance with yield. @@ -1418,7 +1487,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); - _mToken.setCurrentIndex(1_210000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_210); @@ -1429,8 +1498,8 @@ contract WrappedMTokenTests is Test { /* ============ accruedYieldOf ============ */ function test_accruedYieldOf_nonEarner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500); @@ -1446,8 +1515,8 @@ contract WrappedMTokenTests is Test { } function test_accruedYieldOf_earner() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setAccountOf(_alice, 500, 500); // 550 balance with yield. @@ -1457,7 +1526,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); - _mToken.setCurrentIndex(1_210000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.accruedYieldOf(_alice), 210); @@ -1492,13 +1561,17 @@ contract WrappedMTokenTests is Test { function test_isEarningEnabled() external { assertFalse(_wrappedMToken.isEarningEnabled()); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); assertTrue(_wrappedMToken.isEarningEnabled()); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _wrappedMToken.setEnableMIndex(0); assertFalse(_wrappedMToken.isEarningEnabled()); + + _wrappedMToken.setEnableMIndex(1_100000000000); + + assertTrue(_wrappedMToken.isEarningEnabled()); } /* ============ claimOverrideRecipientFor ============ */ @@ -1554,31 +1627,47 @@ contract WrappedMTokenTests is Test { function test_currentIndex() external { assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); - _mToken.setCurrentIndex(1_100000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _wrappedMToken.setDisableIndex(1_050000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_050000000000); + + _wrappedMToken.setDisableIndex(1_100000000000); assertEq(_wrappedMToken.currentIndex(), 1_100000000000); - _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - assertEq(_wrappedMToken.currentIndex(), 1_210000000000); + assertEq(_wrappedMToken.currentIndex(), 1_331000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_155000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_267619047619); + + _wrappedMToken.setEnableMIndex(1_210000000000); assertEq(_wrappedMToken.currentIndex(), 1_210000000000); - _mToken.setCurrentIndex(1_331000000000); + _wrappedMToken.setEnableMIndex(1_270500000000); + + assertEq(_wrappedMToken.currentIndex(), 1_152380952380); + + _wrappedMToken.setEnableMIndex(1_331000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); + + _mToken.setCurrentIndex(1_464100000000); assertEq(_wrappedMToken.currentIndex(), 1_210000000000); } /* ============ excess ============ */ function test_excess() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); assertEq(_wrappedMToken.excess(), 0); @@ -1602,7 +1691,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.excess(), 1_002); - _mToken.setCurrentIndex(1_210000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.excess(), 892); } @@ -1610,13 +1699,19 @@ contract WrappedMTokenTests is Test { function testFuzz_excess( bool earningEnabled_, uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, uint240 totalNonEarningSupply_, uint240 totalProjectedEarningSupply_, uint240 mBalance_ ) external { - currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _setupIndexes(earningEnabled_, currentMIndex_); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); uint240 maxAmount_ = _getMaxAmount(_wrappedMToken.currentIndex()); @@ -1649,8 +1744,8 @@ contract WrappedMTokenTests is Test { /* ============ totalAccruedYield ============ */ function test_totalAccruedYield() external { - _mToken.setCurrentIndex(1_100000000000); - _wrappedMToken.pushEnableDisableEarningIndex(1_000000000000); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalEarningPrincipal(909); _wrappedMToken.setTotalEarningSupply(1_000); @@ -1665,7 +1760,7 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalAccruedYield(), 200); - _mToken.setCurrentIndex(1_210000000000); + _mToken.setCurrentIndex(1_331000000000); assertEq(_wrappedMToken.totalAccruedYield(), 310); } @@ -1683,12 +1778,33 @@ contract WrappedMTokenTests is Test { return (uint240(type(uint112).max) * index_) / _EXP_SCALED_ONE; } - function _setupIndexes(bool earningEnabled_, uint128 currentMIndex_) internal { + function _getFuzzedIndices( + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) internal pure returns (uint128, uint128, uint128) { + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + enableMIndex_ = uint128(bound(enableMIndex_, _EXP_SCALED_ONE, currentMIndex_)); + + disableIndex_ = uint128( + bound(disableIndex_, _EXP_SCALED_ONE, (currentMIndex_ * _EXP_SCALED_ONE) / enableMIndex_) + ); + + return (currentMIndex_, enableMIndex_, disableIndex_); + } + + function _setupIndexes( + bool earningEnabled_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) internal { _mToken.setCurrentIndex(currentMIndex_); + _wrappedMToken.setDisableIndex(disableIndex_); if (earningEnabled_) { _mToken.setIsEarning(address(_wrappedMToken), true); - _wrappedMToken.pushEnableDisableEarningIndex(_EXP_SCALED_ONE); + _wrappedMToken.setEnableMIndex(enableMIndex_); } } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index c0aba64..578a358 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -52,7 +52,11 @@ contract WrappedMTokenHarness is WrappedMToken { totalEarningPrincipal = uint112(totalEarningPrincipal_); } - function pushEnableDisableEarningIndex(uint128 index_) external { - _enableDisableEarningIndices.push(index_); + function setEnableMIndex(uint256 enableMIndex_) external { + enableMIndex = uint128(enableMIndex_); + } + + function setDisableIndex(uint256 disableIndex_) external { + disableIndex = uint128(disableIndex_); } }