Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: enable/disable earning in HubPortal #20

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 34 additions & 43 deletions src/HubPortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Copy link
Collaborator

@toninorair toninorair Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why names_disablePortalIndex and _enableMTokenIndex are not symmetrical? why private and not internal?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_disablePortalIndex is the last index of the Portal when earning was disabled, it's "virtual" index.
_enableMTokenIndex is the index of M Token when earning was enabled, the "actual" index.
Private because we're not planning to inherit from HubPortal


/* ============ Constructor ============ */

Expand All @@ -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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this check is required? isn't it 0 by default on initialize?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to set _disablePortalIndex to EXP_SCALED_ONE only during first initialization, and do not override it after

_disablePortalIndex = IndexingMath.EXP_SCALED_ONE;
}
}

/* ============ Interactive Functions ============ */

/// @inheritdoc IHubPortal
Expand Down Expand Up @@ -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();
toninorair marked this conversation as resolved.
Show resolved Hide resolved
if (!_isEarningEnabled()) revert EarningIsDisabled();

IMTokenLike mToken_ = IMTokenLike(mToken());
uint128 currentMIndex_ = mToken_.currentIndex();
_enableDisableEarningIndices.push(currentMIndex_);
uint128 portalIndex_ = _currentIndex();
_disablePortalIndex = portalIndex_;
_enableMTokenIndex = 0;
PierrickGT marked this conversation as resolved.
Show resolved Hide resolved

mToken_.stopEarning();
IMTokenLike(mToken()).stopEarning(address(this));

emit EarningDisabled(currentMIndex_);
emit EarningDisabled(portalIndex_);
}

/* ============ Internal Interactive Functions ============ */
Expand Down Expand Up @@ -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) {
toninorair marked this conversation as resolved.
Show resolved Hide resolved
return IMTokenLike(mToken()).isEarning(address(this));
function _isEarningEnabled() private view returns (bool) {
return _enableMTokenIndex != 0;
}
}
4 changes: 2 additions & 2 deletions src/interfaces/IMTokenLike.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
11 changes: 5 additions & 6 deletions test/fork/HubPortalFork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 ============ */
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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(
Expand All @@ -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);
}
}
4 changes: 2 additions & 2 deletions test/fork/SpokePortalFork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 6 additions & 2 deletions test/mocks/MockMToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
95 changes: 48 additions & 47 deletions test/unit/HubPortal.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,74 +52,81 @@ 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();
}

function test_enableEarning() external {
uint128 currentMIndex_ = 1_100000068703;

_mToken.setCurrentIndex(currentMIndex_);
_registrar.set(_EARNERS_LIST_IGNORED, bytes32("1"));

vm.expectEmit();
emit IHubPortal.EarningEnabled(currentMIndex_);
Expand All @@ -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();
Expand All @@ -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();
}

Expand All @@ -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(
Expand Down
Loading