Assessment reports>Hyperbeat Pay>Low findings>Balance calculation in MorphoService does not account for loan market assets
Category: Business Logic

Balance calculation in MorphoService does not account for loan market assets

Low Impact
Low Severity
High Likelihood

Description

The getBalance function in MorphoService only accounts for collateral positions and ignores assets supplied to loan markets for earning interest:

// ManagementAccount.sol
function getBalance(address token) external view override returns (uint256 balance) {
    balance = IERC20(token).balanceOf(address(this));

    uint256 length = _trackedServices.length;
    for (uint256 i; i < length; ++i) {
        address service = _trackedServices[i];
        if (service == address(0)) continue;
        if (!_approvedServices[service]) continue;
        try IService(service).getBalance(token, address(this)) returns (uint256 serviceBalance) {
!           balance += serviceBalance;
        } catch { }
    }
}

// MorphoService.sol
function getBalance(address asset, address account) external view override returns (uint256 balance) {
!   Id marketId = collateralMarket[asset];
    if (!isMarketRegistered[marketId]) return 0;

    Position memory pos = morpho.position(marketId, account);
!   balance = pos.collateral;
}

When users supply assets to Morpho loan markets to earn interest, those positions are not included in the balance calculation returned by MorphoService::getBalance. This causes ManagementAccount::getBalance to underreport the account's actual balance when aggregating across services.

Impact

Users see incorrect balance information in UI and off-chain systems that rely on getBalance for display purposes. The actual on-chain functionality remains unaffected as critical operations use direct balance queries.

Recommendations

Update MorphoService::getBalance to include both collateral and supply positions. Note that supply positions in Morpho are denominated in shares, which should be converted to asset amounts:

 function getBalance(address asset, address account) external view override returns (uint256 balance) {
     Id marketId = collateralMarket[asset];
-    if (!isMarketRegistered[marketId]) return 0;
-
-    Position memory pos = morpho.position(marketId, account);
-    balance = pos.collateral;
+    if (isMarketRegistered[marketId]) {
+        Position memory pos = morpho.position(marketId, account);
+        balance += pos.collateral;
+    }
+    
+    // Also check loan markets where asset is supplied
+    Id loanMarketId = loanMarket[asset];
+    if (isMarketRegistered[loanMarketId]) {
+        MarketParams memory params = marketParamsById[loanMarketId];
+        balance += MorphoBalancesLib.expectedSupplyAssets(morpho, params, account);
+    }
 }

Note that this change could break the existing usage of getBalance in ManagementAccountLib::executeServiceCalls.

To address this, we recommend introducing a dedicated getCollateralBalance function that returns only the collateral market token balance. Then, update ManagementAccountLib::executeServiceCalls to use getCollateralBalance in place of getBalance wherever the collateral market balance is specifically required.

Remediation

This issue has been acknowledged by Hyperbeat, and fixes were implemented in the following commits:

Zellic © 2025Back to top ↑