diff --git a/src/HubPortal.sol b/src/HubPortal.sol index b60f389..ad3c85b 100644 --- a/src/HubPortal.sol +++ b/src/HubPortal.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.26; import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol"; +import { UIntMath } from "../lib/common/src/libs/UIntMath.sol"; +import { IndexingMath } from "../lib/common/src/libs/IndexingMath.sol"; import { TransceiverStructs } from "../lib/example-native-token-transfers/evm/src/libraries/TransceiverStructs.sol"; import { IMTokenLike } from "./interfaces/IMTokenLike.sol"; @@ -25,14 +27,11 @@ contract HubPortal is IHubPortal, Portal { /* ============ Variables ============ */ - /// @dev Registrar key holding value of whether the earners list can be ignored or not. - bytes32 internal constant _EARNERS_LIST_IGNORED = "earners_list_ignored"; + /// @dev The Hub Portal's index when earning was most recently disabled + uint128 private _disablePortalIndex; - /// @dev Registrar key of earners list. - bytes32 internal constant _EARNERS_LIST = "earners"; - - /// @dev Array of indices at which earning was enabled or disabled. - uint128[] internal _enableDisableEarningIndices; + /// @dev The M token's index when earning was most recently enabled + uint128 private _enableMTokenIndex; /* ============ Constructor ============ */ @@ -48,6 +47,15 @@ contract HubPortal is IHubPortal, Portal { uint16 chainId_ ) Portal(mToken_, registrar_, Mode.LOCKING, chainId_) {} + function _initialize() internal override { + super._initialize(); + + // set _disablePortalIndex to the default value on first deployment + if (_disablePortalIndex == 0) { + _disablePortalIndex = IndexingMath.EXP_SCALED_ONE; + } + } + /* ============ Interactive Functions ============ */ /// @inheritdoc IHubPortal @@ -91,34 +99,27 @@ contract HubPortal is IHubPortal, Portal { /// @inheritdoc IHubPortal function enableEarning() external { - if (!_isApprovedEarner()) revert NotApprovedEarner(); 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 (_enableDisableEarningIndices.length != 0) revert EarningCannotBeReenabled(); + uint128 mTokenIndex_ = _currentMTokenIndex(); + _enableMTokenIndex = mTokenIndex_; - IMTokenLike mToken_ = IMTokenLike(mToken()); - uint128 currentMIndex_ = mToken_.currentIndex(); - _enableDisableEarningIndices.push(currentMIndex_); + IMTokenLike(mToken()).startEarning(); - mToken_.startEarning(); - - emit EarningEnabled(currentMIndex_); + emit EarningEnabled(mTokenIndex_); } /// @inheritdoc IHubPortal function disableEarning() external { - if (_isApprovedEarner()) revert IsApprovedEarner(); if (!_isEarningEnabled()) revert EarningIsDisabled(); - IMTokenLike mToken_ = IMTokenLike(mToken()); - uint128 currentMIndex_ = mToken_.currentIndex(); - _enableDisableEarningIndices.push(currentMIndex_); + uint128 portalIndex_ = _currentIndex(); + _disablePortalIndex = portalIndex_; + _enableMTokenIndex = 0; - mToken_.stopEarning(); + IMTokenLike(mToken()).stopEarning(address(this)); - emit EarningDisabled(currentMIndex_); + emit EarningDisabled(portalIndex_); } /* ============ Internal Interactive Functions ============ */ @@ -168,33 +169,23 @@ contract HubPortal is IHubPortal, Portal { return TransceiverStructs.nttManagerMessageDigest(chainId, message_); } - /* ============ Internal View/Pure Functions ============ */ + /* ============ Internal/Private View Functions ============ */ - /// @dev Returns the current M token index used by the Hub Portal. + /// @dev Returns the current Hub Portal index function _currentIndex() internal view override returns (uint128) { - if (_isEarningEnabled()) { - return IMTokenLike(mToken()).currentIndex(); - } - - // If earning has been enabled in the past, return the latest recorded index when it was disabled. - // Otherwise, return the starting index. return - _enableDisableEarningIndices.length != 0 - ? _enableDisableEarningIndices[_enableDisableEarningIndices.length - 1] - : 0; + _isEarningEnabled() + ? UIntMath.bound128((uint256(_disablePortalIndex) * _currentMTokenIndex()) / _enableMTokenIndex) + : _disablePortalIndex; } - /// @dev Returns whether the Hub Portal is a TTG-approved earner or not. - function _isApprovedEarner() internal view returns (bool) { - IRegistrarLike registrar_ = IRegistrarLike(registrar); - - return - registrar_.get(_EARNERS_LIST_IGNORED) != bytes32(0) || - registrar_.listContains(_EARNERS_LIST, address(this)); + /// @dev Returns the current M Token index + function _currentMTokenIndex() private view returns (uint128) { + return IMTokenLike(mToken()).currentIndex(); } /// @dev Returns whether earning was enabled for HubPortal or not. - function _isEarningEnabled() internal view returns (bool) { - return IMTokenLike(mToken()).isEarning(address(this)); + function _isEarningEnabled() private view returns (bool) { + return _enableMTokenIndex != 0; } } diff --git a/src/interfaces/IMTokenLike.sol b/src/interfaces/IMTokenLike.sol index ed9f93d..2b5e997 100644 --- a/src/interfaces/IMTokenLike.sol +++ b/src/interfaces/IMTokenLike.sol @@ -20,6 +20,6 @@ interface IMTokenLike { /// @notice Starts earning for caller if allowed by TTG. function startEarning() external; - /// @notice Stops earning for caller. - function stopEarning() external; + /// @notice Stops earning for the account. + function stopEarning(address account_) external; } diff --git a/test/fork/HubPortalFork.t.sol b/test/fork/HubPortalFork.t.sol index 3addca5..75fc470 100644 --- a/test/fork/HubPortalFork.t.sol +++ b/test/fork/HubPortalFork.t.sol @@ -57,7 +57,7 @@ contract HubPortalForkTests is ForkTestBase { _deliverMessage(_BASE_WORMHOLE_RELAYER, signedMessage_); assertEq(IERC20(_baseSpokeMToken).balanceOf(_mHolder), amount_); - assertEq(IContinuousIndexing(_baseSpokeMToken).currentIndex(), mainnetIndex_); + assertEq(IContinuousIndexing(_baseSpokeMToken).currentIndex(), _EXP_SCALED_ONE); } /* ============ sendMTokenIndex ============ */ @@ -88,8 +88,8 @@ contract HubPortalForkTests is ForkTestBase { _deliverMessage(_BASE_WORMHOLE_RELAYER, signedMessage_); - assertEq(IPortal(_baseSpokePortal).currentIndex(), mainnetIndex_); - assertEq(IContinuousIndexing(_baseSpokeMToken).currentIndex(), mainnetIndex_); + assertEq(IPortal(_baseSpokePortal).currentIndex(), _EXP_SCALED_ONE); + assertEq(IContinuousIndexing(_baseSpokeMToken).currentIndex(), _EXP_SCALED_ONE); vm.stopPrank(); } @@ -169,7 +169,7 @@ contract HubPortalForkTests is ForkTestBase { vm.selectFork(_mainnetForkId); uint128 mainnetIndex_ = IContinuousIndexing(_MAINNET_M_TOKEN).currentIndex(); - assertEq(IHubPortal(_hubPortal).currentIndex(), mainnetIndex_); + assertEq(IHubPortal(_hubPortal).currentIndex(), _EXP_SCALED_ONE); // Disable earning for the Hub Portal vm.mockCall( @@ -183,7 +183,6 @@ contract HubPortalForkTests is ForkTestBase { // Move forward by 7 days vm.warp(block.timestamp + 604800); - assertEq(IHubPortal(_hubPortal).currentIndex(), mainnetIndex_); - assertGt(IContinuousIndexing(_MAINNET_M_TOKEN).currentIndex(), IHubPortal(_hubPortal).currentIndex()); + assertEq(IHubPortal(_hubPortal).currentIndex(), _EXP_SCALED_ONE); } } diff --git a/test/fork/SpokePortalFork.t.sol b/test/fork/SpokePortalFork.t.sol index 0350a2c..491fe3d 100644 --- a/test/fork/SpokePortalFork.t.sol +++ b/test/fork/SpokePortalFork.t.sol @@ -81,7 +81,7 @@ contract SpokePortalForkTests is ForkTestBase { _deliverMessage(_OPTIMISM_WORMHOLE_RELAYER, spokeSignedMessage_); assertEq(IERC20(_optimismSpokeMToken).balanceOf(_mHolder), _amount); - assertEq(IContinuousIndexing(_optimismSpokeMToken).currentIndex(), _mainnetIndex); + assertEq(IContinuousIndexing(_optimismSpokeMToken).currentIndex(), _EXP_SCALED_ONE); } function _beforeTest() internal { @@ -117,7 +117,7 @@ contract SpokePortalForkTests is ForkTestBase { _deliverMessage(_BASE_WORMHOLE_RELAYER, hubSignedMessage_); assertEq(IERC20(_baseSpokeMToken).balanceOf(_mHolder), _amount); - assertEq(IContinuousIndexing(_baseSpokeMToken).currentIndex(), _mainnetIndex); + assertEq(IContinuousIndexing(_baseSpokeMToken).currentIndex(), _EXP_SCALED_ONE); // TODO: add excess test once underflow has been fixed // ISpokePortal(_baseSpokePortal).excess(); diff --git a/test/mocks/MockMToken.sol b/test/mocks/MockMToken.sol index 7cefdf4..0f57b65 100644 --- a/test/mocks/MockMToken.sol +++ b/test/mocks/MockMToken.sol @@ -19,7 +19,11 @@ contract MockMToken is MockERC20 { isEarning[account_] = isEarning_; } - function startEarning() external {} + function startEarning() external { + isEarning[msg.sender] = true; + } - function stopEarning() external {} + function stopEarning(address account_) external { + isEarning[account_] = false; + } } diff --git a/test/unit/HubPortal.t.sol b/test/unit/HubPortal.t.sol index bf3a34f..2a62278 100644 --- a/test/unit/HubPortal.t.sol +++ b/test/unit/HubPortal.t.sol @@ -52,66 +52,74 @@ contract HubPortalTests is UnitTestBase { /* ============ currentIndex ============ */ function test_currentIndex_initialState() external { - assertEq(_portal.currentIndex(), 0); + assertEq(_portal.currentIndex(), _EXP_SCALED_ONE); } function test_currentIndex_earningEnabled() external { - uint128 index_ = 1_100000068703; + _mToken.setCurrentIndex(1.1e12); - _mToken.setCurrentIndex(index_); - _mToken.setIsEarning(address(_portal), true); + assertEq(_portal.currentIndex(), _EXP_SCALED_ONE); + + _portal.enableEarning(); - assertEq(_portal.currentIndex(), index_); + // HubPortal index doesn't change + assertEq(_portal.currentIndex(), _EXP_SCALED_ONE); + + _mToken.setCurrentIndex(2.2e12); + + // HubPortal index updated proportionally to M Token index update + assertEq(_portal.currentIndex(), 2e12); } - function test_currentIndex_earningEnabledInThePast() external { - uint128 index_ = 1_100000068703; - uint128 latestIndex_ = 1_200000068703; + function test_currentIndex_earningDisabled() external { + _mToken.setCurrentIndex(1.1e12); - _mToken.setCurrentIndex(index_); - _mToken.setIsEarning(address(_portal), true); + _portal.enableEarning(); - assertEq(_portal.currentIndex(), index_); + _mToken.setCurrentIndex(2.2e12); - _mToken.setCurrentIndex(latestIndex_); + // HubPortal index updated proportionally to M Token index update + assertEq(_portal.currentIndex(), 2e12); _portal.disableEarning(); + _mToken.setCurrentIndex(3.3e12); - _mToken.setIsEarning(address(_portal), false); - _mToken.setCurrentIndex(1_300000068703); - - assertEq(_portal.currentIndex(), latestIndex_); + // HubPortal index doesn't change + assertEq(_portal.currentIndex(), 2e12); } - /* ============ enableEarning ============ */ - - function test_enableEarning_notApprovedEarner() external { - vm.expectRevert(abi.encodeWithSelector(IHubPortal.NotApprovedEarner.selector)); + function test_currentIndex_earningReenabled() external { + _mToken.setCurrentIndex(1.1e12); _portal.enableEarning(); - } - function test_enableEarning_earningIsEnabled() external { - _registrar.setListContains(_EARNERS_LIST, address(_portal), true); - _mToken.setIsEarning(address(_portal), true); + _mToken.setCurrentIndex(2.2e12); - vm.expectRevert(IHubPortal.EarningIsEnabled.selector); - _portal.enableEarning(); - } + // HubPortal index updated proportionally to M Token index update + assertEq(_portal.currentIndex(), 2e12); + + _portal.disableEarning(); + _mToken.setCurrentIndex(3.3e12); - function test_enableEarning_earningCannotBeReenabled() external { - _registrar.setListContains(_EARNERS_LIST, address(_portal), true); + // HubPortal index doesn't change + assertEq(_portal.currentIndex(), 2e12); _portal.enableEarning(); - _mToken.setIsEarning(address(_portal), true); - _registrar.setListContains(_EARNERS_LIST, address(_portal), false); + // HubPortal index doesn't change + assertEq(_portal.currentIndex(), 2e12); - _portal.disableEarning(); + _mToken.setCurrentIndex(6.6e12); + // HubPortal index updated proportionally to M Token index update + assertEq(_portal.currentIndex(), 4e12); + } - _mToken.setIsEarning(address(_portal), false); - _registrar.setListContains(_EARNERS_LIST, address(_portal), true); + /* ============ enableEarning ============ */ - vm.expectRevert(IHubPortal.EarningCannotBeReenabled.selector); + function test_enableEarning_earningIsEnabled() external { + _mToken.setCurrentIndex(1.1e12); + _portal.enableEarning(); + + vm.expectRevert(IHubPortal.EarningIsEnabled.selector); _portal.enableEarning(); } @@ -119,7 +127,6 @@ contract HubPortalTests is UnitTestBase { uint128 currentMIndex_ = 1_100000068703; _mToken.setCurrentIndex(currentMIndex_); - _registrar.set(_EARNERS_LIST_IGNORED, bytes32("1")); vm.expectEmit(); emit IHubPortal.EarningEnabled(currentMIndex_); @@ -130,13 +137,6 @@ contract HubPortalTests is UnitTestBase { /* ============ disableEarning ============ */ - function test_disableEarning_approvedEarner() external { - _registrar.set(_EARNERS_LIST_IGNORED, bytes32("1")); - - vm.expectRevert(IHubPortal.IsApprovedEarner.selector); - _portal.disableEarning(); - } - function test_disableEarning_earningIsDisabled() external { vm.expectRevert(IHubPortal.EarningIsDisabled.selector); _portal.disableEarning(); @@ -146,12 +146,12 @@ contract HubPortalTests is UnitTestBase { uint128 currentMIndex_ = 1_100000068703; _mToken.setCurrentIndex(currentMIndex_); - _mToken.setIsEarning(address(_portal), true); + _portal.enableEarning(); vm.expectEmit(); - emit IHubPortal.EarningDisabled(currentMIndex_); + emit IHubPortal.EarningDisabled(_EXP_SCALED_ONE); - vm.expectCall(address(_mToken), abi.encodeCall(_mToken.stopEarning, ())); + vm.expectCall(address(_mToken), abi.encodeCall(_mToken.stopEarning, (address(_portal)))); _portal.disableEarning(); } @@ -162,8 +162,9 @@ contract HubPortalTests is UnitTestBase { uint256 fee_ = 1; bytes32 refundAddress_ = _alice.toBytes32(); + _mToken.setCurrentIndex(_EXP_SCALED_ONE); + _portal.enableEarning(); _mToken.setCurrentIndex(index_); - _mToken.setIsEarning(address(_portal), true); vm.deal(_alice, fee_); (TransceiverStructs.NttManagerMessage memory message_, bytes32 messageId_) = _createMessage(