diff --git a/src/MigratorV1.sol b/src/MigratorV1.sol index b148337..283c006 100644 --- a/src/MigratorV1.sol +++ b/src/MigratorV1.sol @@ -2,11 +2,15 @@ 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 +21,71 @@ contract MigratorV1 { } fallback() external virtual { + (bool earningEnabled_, uint128 disableIndex_) = _clearEnableDisableEarningIndices(); + + if (earningEnabled_) { + _setEnableMIndex(IndexingMath.EXP_SCALED_ONE); + } else { + _setDisableIndex(disableIndex_); + } + address implementationV2_ = implementationV2; assembly { sstore(_IMPLEMENTATION_SLOT, implementationV2_) } } + + /** + * @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 b118715..1f6c562 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -84,8 +84,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 ============ */ @@ -149,17 +152,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 @@ -168,28 +163,21 @@ 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 function startEarningFor(address[] calldata accounts_) external { - if (!isEarningEnabled()) revert EarningIsDisabled(); - - uint128 currentIndex_ = _currentMIndex(); + uint128 currentIndex_ = currentIndex(); for (uint256 index_; index_ < accounts_.length; ++index_) { _startEarningFor(accounts_[index_], currentIndex_); @@ -258,7 +246,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { /// @inheritdoc IWrappedMToken function currentIndex() public view returns (uint128 index_) { - return isEarningEnabled() ? _currentMIndex() : _lastDisableEarningIndex(); + uint128 disableIndex_ = disableIndex == 0 ? IndexingMath.EXP_SCALED_ONE : disableIndex; + + return enableMIndex == 0 ? disableIndex_ : (disableIndex_ * _currentMIndex()) / enableMIndex; } /// @inheritdoc IWrappedMToken @@ -268,12 +258,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 @@ -656,11 +641,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, last index, and the current index. * @param balance_ The token balance of an earning account. @@ -781,23 +761,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 236df83..366448f 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); @@ -58,11 +58,8 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted when performing an operation that is not allowed when earning is enabled. error EarningIsEnabled(); - /// @notice Emitted when trying to enable earning after it has been explicitly disabled. - error EarningCannotBeReenabled(); - /** - * @notice Emitted when calling `stopEarning` for an account approved as earner by the Registrar. + * @notice Emitted when calling `stopEarning` for an account approved as an earner by the Registrar. * @param account The account that is an approved earner. */ error IsApprovedEarner(address account); @@ -76,7 +73,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended { error InsufficientBalance(address account, uint240 balance, uint240 amount); /** - * @notice Emitted when calling `startEarning` for an account not approved as earner by the Registrar. + * @notice Emitted when calling `startEarning` for an account not approved as an earner by the Registrar. * @param account The account that is not an approved earner. */ error NotApprovedEarner(address account); @@ -227,9 +224,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. @@ -240,9 +243,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 f233dd7..24b1f5c 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -37,8 +37,6 @@ contract WrappedMTokenTests is Test { address[] internal _accounts = [_alice, _bob, _charlie, _david]; - uint128 internal _currentIndex; - MockM internal _mToken; MockRegistrar internal _registrar; WrappedMTokenHarness internal _implementation; @@ -48,7 +46,6 @@ contract WrappedMTokenTests is Test { _registrar = new MockRegistrar(); _mToken = new MockM(); - _mToken.setCurrentIndex(_EXP_SCALED_ONE); _implementation = new WrappedMTokenHarness( address(_mToken), @@ -58,8 +55,6 @@ contract WrappedMTokenTests is Test { ); _wrappedMToken = WrappedMTokenHarness(address(new Proxy(address(_implementation)))); - - _mToken.setCurrentIndex(_currentIndex = 1_100000068703); } /* ============ constructor ============ */ @@ -72,6 +67,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 { @@ -127,87 +124,112 @@ contract WrappedMTokenTests is Test { function test_wrap_toNonEarner() external { _mToken.setBalanceOf(_alice, 1_000); - vm.prank(_alice); - assertEq(_wrappedMToken.wrap(_alice, 1_000), 1_000); + _wrappedMToken.setTotalNonEarningSupply(1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000); + assertEq(_wrappedMToken.lastIndexOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + vm.prank(_alice); + assertEq(_wrappedMToken.wrap(_alice, 1_000), 1_000); + + assertEq(_wrappedMToken.lastIndexOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 2_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 2_000); assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_wrap_toEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.enableEarning(); + _mToken.setBalanceOf(_alice, 1_002); - _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); - _mToken.setBalanceOf(_alice, 1_002); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); // 1_100 balance with yield. + + assertEq(_wrappedMToken.lastIndexOf(_alice), _EXP_SCALED_ONE); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 100); vm.prank(_alice); assertEq(_wrappedMToken.wrap(_alice, 999), 999); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 999); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 909); - assertEq(_wrappedMToken.totalEarningSupply(), 999); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 1_000 + 909); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.prank(_alice); assertEq(_wrappedMToken.wrap(_alice, 1), 1); // No change due to principal round down on wrap. - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999 + 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 910); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 1_000 + 909 + 1); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999 + 1); + assertEq(_wrappedMToken.totalAccruedYield(), 1); vm.prank(_alice); assertEq(_wrappedMToken.wrap(_alice, 2), 2); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 1_002); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000 + 100 + 999 + 1 + 2); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 912); - assertEq(_wrappedMToken.totalEarningSupply(), 1_002); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 1_000 + 909 + 1 + 2); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000 + 100 + 999 + 1 + 2); + assertEq(_wrappedMToken.totalAccruedYield(), 1); } function testFuzz_wrap( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - _wrappedMToken.setTotalEarningSupply(balance_); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(currentIndex_) - balance_)); + wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(_wrappedMToken.currentIndex()) - balanceWithYield_)); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); _mToken.setBalanceOf(_alice, wrapAmount_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -235,37 +257,31 @@ contract WrappedMTokenTests is Test { function testFuzz_wrapFull( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, uint240 wrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - _wrappedMToken.setTotalEarningSupply(balance_); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); - wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(currentIndex_) - balance_)); + wrapAmount_ = uint240(bound(wrapAmount_, 0, _getMaxAmount(_wrappedMToken.currentIndex()) - balanceWithYield_)); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); _mToken.setBalanceOf(_alice, wrapAmount_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -306,11 +322,10 @@ contract WrappedMTokenTests is Test { } function test_unwrap_insufficientBalance_fromEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 909, _EXP_SCALED_ONE); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); vm.prank(_alice); @@ -318,92 +333,138 @@ contract WrappedMTokenTests is Test { } function test_unwrap_fromNonEarner() external { + _mToken.setIsEarning(address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + _mToken.setBalanceOf(address(_wrappedMToken), 1_000); + _wrappedMToken.setTotalNonEarningSupply(1_000); _wrappedMToken.setAccountOf(_alice, 1_000); - _mToken.setBalanceOf(address(_wrappedMToken), 1_000); + assertEq(_wrappedMToken.lastIndexOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + vm.prank(_alice); + assertEq(_wrappedMToken.unwrap(_alice, 1), 0); + + assertEq(_wrappedMToken.lastIndexOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 999); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 999); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 500), 500); + assertEq(_wrappedMToken.unwrap(_alice, 499), 498); + assertEq(_wrappedMToken.lastIndexOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 500); - assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 500), 500); + assertEq(_wrappedMToken.unwrap(_alice, 500), 499); + assertEq(_wrappedMToken.lastIndexOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), 0); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_unwrap_fromEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setIsEarning(address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.enableEarning(); + _mToken.setBalanceOf(address(_wrappedMToken), 1_000); _wrappedMToken.setPrincipalOfTotalEarningSupply(909); - _wrappedMToken.setTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningSupply(909); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 909, _EXP_SCALED_ONE); // 999 balance with yield. - _mToken.setBalanceOf(address(_wrappedMToken), 1_000); + assertEq(_wrappedMToken.lastIndexOf(_alice), _EXP_SCALED_ONE); + assertEq(_wrappedMToken.balanceOf(_alice), 909); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 90); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 909); + assertEq(_wrappedMToken.totalEarningSupply(), 909); + assertEq(_wrappedMToken.totalAccruedYield(), 90); vm.prank(_alice); assertEq(_wrappedMToken.unwrap(_alice, 1), 0); // Change due to principal round up on unwrap. - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 999); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 999); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 909 - 0); + assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1); + assertEq(_wrappedMToken.totalAccruedYield(), 1); vm.prank(_alice); - assertEq(_wrappedMToken.unwrap(_alice, 999), 998); + assertEq(_wrappedMToken.unwrap(_alice, 498), 497); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 0); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1 - 498); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 909 - 0 - 452); + assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1 - 498); + assertEq(_wrappedMToken.totalAccruedYield(), 2); + + vm.prank(_alice); + assertEq(_wrappedMToken.unwrap(_alice, 500), 499); + + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 999 - 1 - 498 - 500); // 0 + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); // 0 due to 0 `totalEarningSupply`. + assertEq(_wrappedMToken.totalEarningSupply(), 999 - 1 - 498 - 500); // 0 + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function testFuzz_unwrap( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, uint240 unwrapAmount_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - _wrappedMToken.setTotalEarningSupply(balance_); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -443,35 +504,27 @@ contract WrappedMTokenTests is Test { function testFuzz_unwrapFull( bool earningEnabled_, bool accountEarning_, + uint240 balanceWithYield_, uint240 balance_, - uint128 accountIndex_, - uint128 currentIndex_ + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ ) external { - accountEarning_ = earningEnabled_ && accountEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - accountIndex_ = uint128(bound(accountIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - - if (accountEarning_) { - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - _wrappedMToken.setTotalEarningSupply(balance_); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(balance_, accountIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, balance_); - _wrappedMToken.setTotalNonEarningSupply(balance_); - } + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); - currentIndex_ = uint128(bound(currentIndex_, accountIndex_, 10 * _EXP_SCALED_ONE)); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); @@ -505,13 +558,16 @@ contract WrappedMTokenTests is Test { } function test_claimFor_earner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.enableEarning(); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); vm.expectEmit(); emit IWrappedMToken.Claimed(_alice, _alice, 100); @@ -521,22 +577,30 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.claimFor(_alice), 100); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_100); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_claimFor_earner_withOverrideRecipient() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _registrar.set( keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), bytes32(uint256(uint160(_bob))) ); - _wrappedMToken.enableEarning(); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); vm.expectEmit(); emit IWrappedMToken.Claimed(_alice, _bob, 100); @@ -549,37 +613,56 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.claimFor(_alice), 100); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_bob), 100); + + assertEq(_wrappedMToken.totalNonEarningSupply(), 100); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 910); + assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalAccruedYield(), 1); } - function testFuzz_claimFor(uint240 balance_, uint128 accountIndex_, uint128 index_, bool claimOverride_) external { - accountIndex_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - index_ = uint128(bound(index_, accountIndex_, 10 * _EXP_SCALED_ONE)); + function testFuzz_claimFor( + bool earningEnabled_, + bool accountEarning_, + uint240 balanceWithYield_, + uint240 balance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, + bool claimOverride_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); + + _setupAccount(_alice, accountEarning_, balanceWithYield_, balance_); if (claimOverride_) { _registrar.set( keccak256(abi.encode(_CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX, _alice)), - bytes32(uint256(uint160(_charlie))) + bytes32(uint256(uint160(_bob))) ); } - _wrappedMToken.enableEarning(); - - _wrappedMToken.setTotalEarningSupply(balance_); - - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); - - _mToken.setCurrentIndex(index_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); if (accruedYield_ != 0) { vm.expectEmit(); - emit IWrappedMToken.Claimed(_alice, claimOverride_ ? _charlie : _alice, accruedYield_); + emit IWrappedMToken.Claimed(_alice, claimOverride_ ? _bob : _alice, accruedYield_); vm.expectEmit(); emit IERC20.Transfer(address(0), _alice, accruedYield_); @@ -587,34 +670,46 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.claimFor(_alice), accruedYield_); - assertEq( - _wrappedMToken.totalSupply(), - _wrappedMToken.balanceOf(_alice) + _wrappedMToken.balanceOf(_bob) + _wrappedMToken.balanceOf(_charlie) - ); + assertEq(_wrappedMToken.totalSupply(), _wrappedMToken.balanceOf(_alice) + _wrappedMToken.balanceOf(_bob)); } /* ============ claimExcess ============ */ function testFuzz_claimExcess( - uint128 index_, + bool earningEnabled_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, uint240 totalNonEarningSupply_, - uint112 principalOfTotalEarningSupply_, + uint240 totalProjectedEarningSupply_, uint240 mBalance_ ) external { - index_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + uint240 maxAmount_ = _getMaxAmount(_wrappedMToken.currentIndex()); - totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, _getMaxAmount(index_))); + totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, maxAmount_)); - uint240 totalEarningSupply_ = uint112(bound(principalOfTotalEarningSupply_, 0, _getMaxAmount(index_))); + totalProjectedEarningSupply_ = uint240( + bound(totalProjectedEarningSupply_, 0, maxAmount_ - totalNonEarningSupply_) + ); - principalOfTotalEarningSupply_ = uint112(totalEarningSupply_ / index_); + uint112 principalOfTotalEarningSupply_ = IndexingMath.getPrincipalAmountRoundedUp( + totalProjectedEarningSupply_, + _wrappedMToken.currentIndex() + ); - mBalance_ = uint240(bound(mBalance_, totalNonEarningSupply_ + totalEarningSupply_, type(uint240).max)); + mBalance_ = uint240(bound(mBalance_, 0, maxAmount_)); _mToken.setBalanceOf(address(_wrappedMToken), mBalance_); - _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); - _wrappedMToken.setPrincipalOfTotalEarningSupply(principalOfTotalEarningSupply_); - _mToken.setCurrentIndex(index_); + _wrappedMToken.setPrincipalOfTotalEarningSupply(principalOfTotalEarningSupply_); + _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); uint240 expectedExcess_ = _wrappedMToken.excess(); @@ -627,7 +722,6 @@ contract WrappedMTokenTests is Test { emit IWrappedMToken.ExcessClaimed(expectedExcess_); assertEq(_wrappedMToken.claimExcess(), expectedExcess_); - assertEq(_wrappedMToken.excess(), 0); } /* ============ transfer ============ */ @@ -640,6 +734,14 @@ contract WrappedMTokenTests is Test { _wrappedMToken.transfer(address(0), 1_000); } + function test_transfer_insufficientBalance_toSelf() external { + _wrappedMToken.setAccountOf(_alice, 999); + + vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); + vm.prank(_alice); + _wrappedMToken.transfer(_alice, 1_000); + } + function test_transfer_insufficientBalance_fromNonEarner_toNonEarner() external { _wrappedMToken.setAccountOf(_alice, 999); @@ -649,11 +751,10 @@ contract WrappedMTokenTests is Test { } function test_transfer_insufficientBalance_fromEarner_toNonEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 909, _EXP_SCALED_ONE); vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.InsufficientBalance.selector, _alice, 999, 1_000)); vm.prank(_alice); @@ -677,8 +778,8 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_bob), 1_000); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_500); - assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); } function testFuzz_transfer_fromNonEarner_toNonEarner( @@ -686,7 +787,7 @@ contract WrappedMTokenTests is Test { uint256 aliceBalance_, uint256 transferAmount_ ) external { - supply_ = bound(supply_, 1, type(uint112).max); + supply_ = bound(supply_, 1, type(uint240).max); aliceBalance_ = bound(aliceBalance_, 1, supply_); transferAmount_ = bound(transferAmount_, 1, aliceBalance_); uint256 bobBalance = supply_ - aliceBalance_; @@ -706,36 +807,46 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_bob), bobBalance + transferAmount_); assertEq(_wrappedMToken.totalNonEarningSupply(), supply_); - assertEq(_wrappedMToken.totalEarningSupply(), 0); assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); } function test_transfer_fromEarner_toNonEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); _wrappedMToken.setTotalEarningSupply(1_000); _wrappedMToken.setTotalNonEarningSupply(500); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); // 1_100 balance with yield. _wrappedMToken.setAccountOf(_bob, 500); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); vm.prank(_alice); _wrappedMToken.transfer(_bob, 500); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 600); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_bob), 1_000); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalEarningSupply(), 500); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 546); + assertEq(_wrappedMToken.totalEarningSupply(), 600); + assertEq(_wrappedMToken.totalAccruedYield(), 0); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 1); @@ -743,27 +854,37 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.transfer(_bob, 1); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 499); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 599); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_bob), 1_001); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_001); - assertEq(_wrappedMToken.totalEarningSupply(), 499); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 546); + assertEq(_wrappedMToken.totalEarningSupply(), 599); + assertEq(_wrappedMToken.totalAccruedYield(), 1); } function test_transfer_fromNonEarner_toEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(454); + _wrappedMToken.setPrincipalOfTotalEarningSupply(500); _wrappedMToken.setTotalEarningSupply(500); _wrappedMToken.setTotalNonEarningSupply(1_000); _wrappedMToken.setAccountOf(_alice, 1_000); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex); + _wrappedMToken.setAccountOf(_bob, 500, _EXP_SCALED_ONE); // 550 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_bob, _bob, 50); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _bob, 50); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -773,23 +894,40 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 500); - assertEq(_wrappedMToken.lastIndexOf(_bob), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + assertEq(_wrappedMToken.lastIndexOf(_bob), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_bob), 1_050); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 500); - assertEq(_wrappedMToken.totalEarningSupply(), 1_000); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 955); + assertEq(_wrappedMToken.totalEarningSupply(), 1_050); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_transfer_fromEarner_toEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(1_363); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_500); _wrappedMToken.setTotalEarningSupply(1_500); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); - _wrappedMToken.setAccountOf(_bob, 500, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); // 1_100 balance with yield. + _wrappedMToken.setAccountOf(_bob, 500, _EXP_SCALED_ONE); // 550 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 50); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_bob, _bob, 50); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _bob, 50); vm.expectEmit(); emit IERC20.Transfer(_alice, _bob, 500); @@ -797,14 +935,18 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.transfer(_bob, 500); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_alice), 500); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 600); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); - assertEq(_wrappedMToken.lastIndexOf(_bob), _currentIndex); - assertEq(_wrappedMToken.balanceOf(_bob), 1_000); + assertEq(_wrappedMToken.lastIndexOf(_bob), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_bob), 1_050); + assertEq(_wrappedMToken.accruedYieldOf(_bob), 0); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); - assertEq(_wrappedMToken.totalEarningSupply(), 1_500); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 1_500); + assertEq(_wrappedMToken.totalEarningSupply(), 1_650); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function test_transfer_nonEarnerToSelf() external { @@ -821,24 +963,25 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 1_000); assertEq(_wrappedMToken.totalNonEarningSupply(), 1_000); - assertEq(_wrappedMToken.totalEarningSupply(), 0); - assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); } function test_transfer_earnerToSelf() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.enableEarning(); - - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 1_000, _currentIndex); - - _mToken.setCurrentIndex((_currentIndex * 5) / 3); // 1_833333447838 + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - assertEq(_wrappedMToken.accruedYieldOf(_alice), 666); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); vm.expectEmit(); emit IERC20.Transfer(_alice, _alice, 500); @@ -846,66 +989,51 @@ contract WrappedMTokenTests is Test { vm.prank(_alice); _wrappedMToken.transfer(_alice, 500); - assertEq(_wrappedMToken.balanceOf(_alice), 1_666); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 1_000); + assertEq(_wrappedMToken.totalEarningSupply(), 1_100); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } function testFuzz_transfer( bool earningEnabled_, bool aliceEarning_, bool bobEarning_, + uint240 aliceBalanceWithYield_, uint240 aliceBalance_, + uint240 bobBalanceWithYield_, uint240 bobBalance_, - uint128 aliceIndex_, - uint128 bobIndex_, - uint128 currentIndex_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, uint240 amount_ ) external { - aliceEarning_ = earningEnabled_ && aliceEarning_; - bobEarning_ = earningEnabled_ && bobEarning_; - - if (earningEnabled_) { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _wrappedMToken.enableEarning(); - } - - aliceIndex_ = uint128(bound(aliceIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - aliceBalance_ = uint240(bound(aliceBalance_, 0, _getMaxAmount(aliceIndex_) / 4)); - - if (aliceEarning_) { - _wrappedMToken.setAccountOf(_alice, aliceBalance_, aliceIndex_); - _wrappedMToken.setTotalEarningSupply(aliceBalance_); - - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown(aliceBalance_, aliceIndex_) - ); - } else { - _wrappedMToken.setAccountOf(_alice, aliceBalance_); - _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); - } + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - bobIndex_ = uint128(bound(bobIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - bobBalance_ = uint240(bound(bobBalance_, 0, _getMaxAmount(bobIndex_) / 4)); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); - if (bobEarning_) { - _wrappedMToken.setAccountOf(_bob, bobBalance_, bobIndex_); - _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + bobBalance_); + (aliceBalanceWithYield_, aliceBalance_) = _getFuzzedBalances( + aliceBalanceWithYield_, + aliceBalance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _wrappedMToken.setPrincipalOfTotalEarningSupply( - IndexingMath.getPrincipalAmountRoundedDown( - _wrappedMToken.totalEarningSupply() + bobBalance_, - aliceIndex_ > bobIndex_ ? aliceIndex_ : bobIndex_ - ) - ); - } else { - _wrappedMToken.setAccountOf(_bob, bobBalance_); - _wrappedMToken.setTotalNonEarningSupply(_wrappedMToken.totalNonEarningSupply() + bobBalance_); - } + _setupAccount(_alice, aliceEarning_, aliceBalanceWithYield_, aliceBalance_); - currentIndex_ = uint128( - bound(currentIndex_, aliceIndex_ > bobIndex_ ? aliceIndex_ : bobIndex_, 10 * _EXP_SCALED_ONE) + (bobBalanceWithYield_, bobBalance_) = _getFuzzedBalances( + bobBalanceWithYield_, + bobBalance_, + _getMaxAmount(_wrappedMToken.currentIndex()) - aliceBalanceWithYield_ ); - _mToken.setCurrentIndex(_currentIndex = currentIndex_); + _setupAccount(_bob, bobEarning_, bobBalanceWithYield_, bobBalance_); uint240 aliceAccruedYield_ = _wrappedMToken.accruedYieldOf(_alice); uint240 bobAccruedYield_ = _wrappedMToken.accruedYieldOf(_bob); @@ -941,41 +1069,26 @@ contract WrappedMTokenTests is Test { ); } else if (aliceEarning_) { assertEq(_wrappedMToken.totalEarningSupply(), aliceBalance_ + aliceAccruedYield_ - amount_); - assertEq(_wrappedMToken.totalNonEarningSupply(), bobBalance_ + bobAccruedYield_ + amount_); + assertEq(_wrappedMToken.totalNonEarningSupply(), bobBalance_ + amount_); } else if (bobEarning_) { - assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ + aliceAccruedYield_ - amount_); + assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ - amount_); assertEq(_wrappedMToken.totalEarningSupply(), bobBalance_ + bobAccruedYield_ + amount_); } else { - assertEq( - _wrappedMToken.totalNonEarningSupply(), - aliceBalance_ + aliceAccruedYield_ + bobBalance_ + bobAccruedYield_ - ); + assertEq(_wrappedMToken.totalNonEarningSupply(), aliceBalance_ + bobBalance_); } } /* ============ startEarningFor ============ */ - function test_startEarningFor_earningIsDisabled() external { - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.startEarningFor(_alice); - } - function test_startEarningFor_notApprovedEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, _alice)); _wrappedMToken.startEarningFor(_alice); } function test_startEarning_overflow() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - uint256 aliceBalance_ = uint256(type(uint112).max) + 20; + _mToken.setCurrentIndex(1_100000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _mToken.setCurrentIndex(_currentIndex = _EXP_SCALED_ONE); + uint240 aliceBalance_ = uint240(type(uint112).max) + 20; // TODO: _getMaxAmount(1_100000000000) + 2; ? _wrappedMToken.setTotalNonEarningSupply(aliceBalance_); @@ -988,9 +1101,8 @@ contract WrappedMTokenTests is Test { } function test_startEarningFor() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); _wrappedMToken.setTotalNonEarningSupply(1_000); @@ -1004,36 +1116,42 @@ contract WrappedMTokenTests is Test { _wrappedMToken.startEarningFor(_alice); assertEq(_wrappedMToken.isEarning(_alice), true); - assertEq(_wrappedMToken.lastIndexOf(_alice), _currentIndex); + assertEq(_wrappedMToken.lastIndexOf(_alice), 1_100000000000); assertEq(_wrappedMToken.balanceOf(_alice), 1000); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 910); assertEq(_wrappedMToken.totalEarningSupply(), 1_000); } - function testFuzz_startEarningFor(uint240 balance_, uint128 index_) external { - balance_ = uint240(bound(balance_, 0, _getMaxAmount(_currentIndex))); - index_ = uint128(bound(index_, _currentIndex, 10 * _EXP_SCALED_ONE)); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + function testFuzz_startEarningFor( + bool earningEnabled_, + uint240 balance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _wrappedMToken.enableEarning(); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); - _wrappedMToken.setTotalNonEarningSupply(balance_); + balance_ = uint240(bound(balance_, 0, _getMaxAmount(_wrappedMToken.currentIndex()))); - _wrappedMToken.setAccountOf(_alice, balance_); + _setupAccount(_alice, false, 0, balance_); _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _mToken.setCurrentIndex(index_); - vm.expectEmit(); emit IWrappedMToken.StartedEarning(_alice); _wrappedMToken.startEarningFor(_alice); assertEq(_wrappedMToken.isEarning(_alice), true); - assertEq(_wrappedMToken.lastIndexOf(_alice), index_); + assertEq(_wrappedMToken.lastIndexOf(_alice), _wrappedMToken.currentIndex()); assertEq(_wrappedMToken.balanceOf(_alice), balance_); assertEq(_wrappedMToken.totalNonEarningSupply(), 0); @@ -1041,17 +1159,9 @@ contract WrappedMTokenTests is Test { } /* ============ 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); _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _wrappedMToken.enableEarning(); - address[] memory accounts_ = new address[](2); accounts_[0] = _alice; accounts_[1] = _bob; @@ -1061,12 +1171,9 @@ contract WrappedMTokenTests is Test { } function test_startEarningFor_batch() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); - _wrappedMToken.enableEarning(); - address[] memory accounts_ = new address[](2); accounts_[0] = _alice; accounts_[1] = _bob; @@ -1089,54 +1196,86 @@ contract WrappedMTokenTests is Test { } function test_stopEarningFor() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); _wrappedMToken.setTotalEarningSupply(1_000); - _wrappedMToken.setAccountOf(_alice, 999, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); // 1_100 balance with yield. + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, 100); + + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, 100); vm.expectEmit(); emit IWrappedMToken.StoppedEarning(_alice); _wrappedMToken.stopEarningFor(_alice); - assertEq(_wrappedMToken.balanceOf(_alice), 999); + assertEq(_wrappedMToken.lastIndexOf(_alice), 0); + assertEq(_wrappedMToken.balanceOf(_alice), 1_100); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.isEarning(_alice), false); - assertEq(_wrappedMToken.totalNonEarningSupply(), 999); - assertEq(_wrappedMToken.totalEarningSupply(), 1); + assertEq(_wrappedMToken.totalNonEarningSupply(), 1_100); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); + assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } - function testFuzz_stopEarningFor(uint240 balance_, uint128 accountIndex_, uint128 index_) external { - accountIndex_ = uint128(bound(index_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); - balance_ = uint240(bound(balance_, 0, _getMaxAmount(accountIndex_))); - index_ = uint128(bound(index_, accountIndex_, 10 * _EXP_SCALED_ONE)); + function testFuzz_stopEarningFor( + bool earningEnabled_, + uint240 balanceWithYield_, + uint240 balance_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); - _wrappedMToken.enableEarning(); + (balanceWithYield_, balance_) = _getFuzzedBalances( + balanceWithYield_, + balance_, + _getMaxAmount(_wrappedMToken.currentIndex()) + ); - _wrappedMToken.setTotalEarningSupply(balance_); + _setupAccount(_alice, true, balanceWithYield_, balance_); - _wrappedMToken.setAccountOf(_alice, balance_, accountIndex_); + uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); - _mToken.setCurrentIndex(index_); + if (accruedYield_ != 0) { + vm.expectEmit(); + emit IWrappedMToken.Claimed(_alice, _alice, accruedYield_); - uint240 accruedYield_ = _wrappedMToken.accruedYieldOf(_alice); + vm.expectEmit(); + emit IERC20.Transfer(address(0), _alice, accruedYield_); + } vm.expectEmit(); emit IWrappedMToken.StoppedEarning(_alice); _wrappedMToken.stopEarningFor(_alice); + assertEq(_wrappedMToken.lastIndexOf(_alice), 0); assertEq(_wrappedMToken.balanceOf(_alice), balance_ + accruedYield_); + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); assertEq(_wrappedMToken.isEarning(_alice), false); assertEq(_wrappedMToken.totalNonEarningSupply(), balance_ + accruedYield_); + assertEq(_wrappedMToken.principalOfTotalEarningSupply(), 0); assertEq(_wrappedMToken.totalEarningSupply(), 0); + assertEq(_wrappedMToken.totalAccruedYield(), 0); } /* ============ stopEarningFor batch ============ */ @@ -1152,12 +1291,8 @@ contract WrappedMTokenTests is Test { } function test_stopEarningFor_batch() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 0, _currentIndex); - _wrappedMToken.setAccountOf(_bob, 0, _currentIndex); + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_bob, 0, _EXP_SCALED_ONE); address[] memory accounts_ = new address[](2); accounts_[0] = _alice; @@ -1178,45 +1313,23 @@ contract WrappedMTokenTests is Test { _wrappedMToken.enableEarning(); } - function test_enableEarning_earningCannotBeReenabled() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - vm.expectRevert(IWrappedMToken.EarningCannotBeReenabled.selector); - _wrappedMToken.enableEarning(); - } - function test_enableEarning() external { _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _mToken.setCurrentIndex(1_210000000000); + vm.expectEmit(); - emit IWrappedMToken.EarningEnabled(_currentIndex); + emit IWrappedMToken.EarningEnabled(1_210000000000); _wrappedMToken.enableEarning(); + + assertEq(_wrappedMToken.enableMIndex(), 1_210000000000); } /* ============ disableEarning ============ */ function test_disableEarning_earningIsDisabled() external { vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); _wrappedMToken.disableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); - - _wrappedMToken.disableEarning(); - - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.disableEarning(); } function test_disableEarning_approvedEarner() external { @@ -1227,14 +1340,11 @@ contract WrappedMTokenTests is Test { } function test_disableEarning() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); vm.expectEmit(); - emit IWrappedMToken.EarningDisabled(_currentIndex); + emit IWrappedMToken.EarningDisabled(1_100000000000); _wrappedMToken.disableEarning(); } @@ -1251,19 +1361,18 @@ contract WrappedMTokenTests is Test { } function test_balanceOf_earner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); - _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); // 550 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 1_000); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); // 1_100 balance with yield. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _wrappedMToken.setLastIndexOf(_alice, 2 * _EXP_SCALED_ONE); + _wrappedMToken.setLastIndexOf(_alice, 1_100000000000); // Last index has no bearing on balance. assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } @@ -1319,31 +1428,121 @@ contract WrappedMTokenTests is Test { /* ============ currentIndex ============ */ function test_currentIndex() external { - assertEq(_wrappedMToken.currentIndex(), 0); + assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); - _mToken.setCurrentIndex(2 * _EXP_SCALED_ONE); + _mToken.setCurrentIndex(1_331000000000); - assertEq(_wrappedMToken.currentIndex(), 0); + assertEq(_wrappedMToken.currentIndex(), _EXP_SCALED_ONE); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + _wrappedMToken.setDisableIndex(1_050000000000); - _wrappedMToken.enableEarning(); + assertEq(_wrappedMToken.currentIndex(), 1_050000000000); - assertEq(_wrappedMToken.currentIndex(), 2 * _EXP_SCALED_ONE); + _wrappedMToken.setDisableIndex(1_100000000000); - _mToken.setCurrentIndex(3 * _EXP_SCALED_ONE); + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); + _wrappedMToken.setEnableMIndex(1_100000000000); - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); + assertEq(_wrappedMToken.currentIndex(), 1_331000000000); - _wrappedMToken.disableEarning(); + _wrappedMToken.setEnableMIndex(1_155000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_267619047619); + + _wrappedMToken.setEnableMIndex(1_210000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_210000000000); + + _wrappedMToken.setEnableMIndex(1_270500000000); + + assertEq(_wrappedMToken.currentIndex(), 1_152380952380); + + _wrappedMToken.setEnableMIndex(1_331000000000); + + assertEq(_wrappedMToken.currentIndex(), 1_100000000000); + + _mToken.setCurrentIndex(1_464100000000); + + assertEq(_wrappedMToken.currentIndex(), 1_210000000000); + } + + /* ============ excess ============ */ + function test_excess() external { + _mToken.setCurrentIndex(1_210000000000); + _wrappedMToken.setEnableMIndex(1_100000000000); + + assertEq(_wrappedMToken.excess(), 0); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_100); + + assertEq(_wrappedMToken.excess(), 0); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_101); + + assertEq(_wrappedMToken.excess(), 1); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_102); + + assertEq(_wrappedMToken.excess(), 2); + + _mToken.setBalanceOf(address(_wrappedMToken), 3_102); - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); + assertEq(_wrappedMToken.excess(), 1_002); - _mToken.setCurrentIndex(4 * _EXP_SCALED_ONE); + _mToken.setCurrentIndex(1_331000000000); - assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); + assertEq(_wrappedMToken.excess(), 892); + } + + function testFuzz_excess( + bool earningEnabled_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_, + uint240 totalNonEarningSupply_, + uint240 totalProjectedEarningSupply_, + uint240 mBalance_ + ) external { + (currentMIndex_, enableMIndex_, disableIndex_) = _getFuzzedIndices( + currentMIndex_, + enableMIndex_, + disableIndex_ + ); + + _setupIndexes(earningEnabled_, currentMIndex_, enableMIndex_, disableIndex_); + + uint240 maxAmount_ = _getMaxAmount(_wrappedMToken.currentIndex()); + + totalNonEarningSupply_ = uint240(bound(totalNonEarningSupply_, 0, maxAmount_)); + + totalProjectedEarningSupply_ = uint240( + bound(totalProjectedEarningSupply_, 0, maxAmount_ - totalNonEarningSupply_) + ); + + uint112 principalOfTotalEarningSupply_ = IndexingMath.getPrincipalAmountRoundedUp( + totalProjectedEarningSupply_, + _wrappedMToken.currentIndex() + ); + + mBalance_ = uint240(bound(mBalance_, 0, maxAmount_)); + + _mToken.setBalanceOf(address(_wrappedMToken), mBalance_); + + _wrappedMToken.setPrincipalOfTotalEarningSupply(principalOfTotalEarningSupply_); + _wrappedMToken.setTotalNonEarningSupply(totalNonEarningSupply_); + + uint240 totalProjectedSupply_ = totalNonEarningSupply_ + totalProjectedEarningSupply_; + + if (mBalance_ > totalProjectedSupply_) { + assertLe(_wrappedMToken.excess(), mBalance_ - totalProjectedSupply_); + } else { + assertEq(_wrappedMToken.excess(), 0); + } } /* ============ utils ============ */ @@ -1358,4 +1557,73 @@ contract WrappedMTokenTests is Test { function _getMaxAmount(uint128 index_) internal pure returns (uint240 maxAmount_) { return (uint240(type(uint112).max) * index_) / _EXP_SCALED_ONE; } + + function _getFuzzedIndices( + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) internal pure returns (uint128, uint128, uint128) { + currentMIndex_ = uint128(bound(currentMIndex_, _EXP_SCALED_ONE, 10 * _EXP_SCALED_ONE)); + enableMIndex_ = uint128(bound(enableMIndex_, _EXP_SCALED_ONE, currentMIndex_)); + + disableIndex_ = uint128( + bound(disableIndex_, _EXP_SCALED_ONE, (currentMIndex_ * _EXP_SCALED_ONE) / enableMIndex_) + ); + + return (currentMIndex_, enableMIndex_, disableIndex_); + } + + function _setupIndexes( + bool earningEnabled_, + uint128 currentMIndex_, + uint128 enableMIndex_, + uint128 disableIndex_ + ) internal { + _mToken.setCurrentIndex(currentMIndex_); + _wrappedMToken.setDisableIndex(disableIndex_); + + if (earningEnabled_) { + _mToken.setIsEarning(address(_wrappedMToken), true); + _wrappedMToken.setEnableMIndex(enableMIndex_); + } + } + + function _getFuzzedBalances( + uint240 balanceWithYield_, + uint240 balance_, + uint240 maxAmount_ + ) internal view returns (uint240, uint240) { + uint128 currentIndex_ = _wrappedMToken.currentIndex(); + + balanceWithYield_ = uint240(bound(balanceWithYield_, 0, maxAmount_)); + balance_ = uint240(bound(balance_, (balanceWithYield_ * _EXP_SCALED_ONE) / currentIndex_, balanceWithYield_)); + balance_ = balance_ == 0 && balanceWithYield_ != 0 ? 1 : balance_; + + return (balanceWithYield_, balance_); + } + + function _setupAccount( + address account_, + bool accountEarning_, + uint240 balanceWithYield_, + uint240 balance_ + ) internal { + if (accountEarning_) { + uint128 lastIndex_ = balanceWithYield_ == 0 + ? _EXP_SCALED_ONE + : uint128((balance_ * _wrappedMToken.currentIndex()) / balanceWithYield_); + + _wrappedMToken.setAccountOf(account_, balance_, lastIndex_); + + _wrappedMToken.setPrincipalOfTotalEarningSupply( + _wrappedMToken.principalOfTotalEarningSupply() + + IndexingMath.getPrincipalAmountRoundedUp(balance_, lastIndex_) + ); + + _wrappedMToken.setTotalEarningSupply(_wrappedMToken.totalEarningSupply() + balance_); + } else { + _wrappedMToken.setAccountOf(account_, balance_); + _wrappedMToken.setTotalNonEarningSupply(_wrappedMToken.totalNonEarningSupply() + balance_); + } + } } diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index 805eb7b..ea1c836 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -30,6 +30,10 @@ contract MockM { currentIndex = currentIndex_; } + function setIsEarning(address account_, bool isEarning_) external { + isEarning[account_] = isEarning_; + } + function startEarning() external { isEarning[msg.sender] = true; } diff --git a/test/utils/WrappedMTokenHarness.sol b/test/utils/WrappedMTokenHarness.sol index b285c83..185e0aa 100644 --- a/test/utils/WrappedMTokenHarness.sol +++ b/test/utils/WrappedMTokenHarness.sol @@ -40,8 +40,11 @@ contract WrappedMTokenHarness is WrappedMToken { principalOfTotalEarningSupply = uint112(principalOfTotalEarningSupply_); } - function getAccountOf(address account_) external view returns (bool isEarning_, uint240 balance_, uint128 index_) { - Account storage account = _accounts[account_]; - return (account.isEarning, account.balance, account.lastIndex); + function setEnableMIndex(uint256 enableMIndex_) external { + enableMIndex = uint128(enableMIndex_); + } + + function setDisableIndex(uint256 disableIndex_) external { + disableIndex = uint128(disableIndex_); } }