Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 3 min read

How I Found a High-Severity Rounding Vulnerability in Morpho Midnight (Tenor Labs)

Introduction Smart contract security is often about finding the invisible edge cases. While participating in a recent bug bounty for Tenor Labs (a protocol built on top of Morpho Midnight), I discovered a high-severity

Introduction

Smart contract security is often about finding the invisible edge cases. While participating in a recent bug bounty for Tenor Labs (a protocol built on top of Morpho Midnight), I discovered a high-severity rounding vulnerability in their migration callbacks.

If exploited, this flaw allows an attacker to artificially inflate a user's Loan-to-Value (LTV) ratio to infinity, triggering an immediate liquidation of the user's pre-existing collateral.

In this article, I’ll break down the vulnerability, explain the math behind the exploit, and provide the Foundry Proof of Concept (PoC) that proves it works.

The Architecture: Tenor Labs & Morpho Midnight

Tenor Labs is a suite of non-custodial smart contracts that extends the Morpho Midnight protocol. One of its core features is the ability for borrowers to seamlessly migrate their debt and collateral between different markets (e.g., moving a position from Market A to Market B).

This is handled by BorrowMidnightRenewalCallback.sol and the CollateralTransferLib.

When a position is migrated, the protocol calculates how much collateral needs to move from the source market to the target market. It does this pro-rata, based on the amount of debt being repaid.

The Vulnerability: mulDivDown Rounding to Zero

The core of the bug lies in the transferCollaterals function within CollateralTransferLib.sol. The code uses mulDivDown to calculate the collateral to transfer:

if (sourceDebtBefore != repaidUnits) {
    collateralToTransfer = collateralToTransfer.mulDivDown(repaidUnits, sourceDebtBefore);
}

mulDivDown performs multiplication followed by division, always rounding down.

Here is the fatal flaw: In DeFi, debt accrues interest. If a borrower has 1000e18 in collateral and 1000e18 in debt, after one block, their debt might be 1000e18 + 1 wei due to interest.

If an attacker performs a "dust fill" on the migration offer (repaying exactly 1 wei), the math looks like this:

1000e18 * 1 / (1000e18 + 1) = 0.999...

Because mulDivDown rounds down to the nearest integer, the collateralToTransfer becomes 0.

The Exploit Scenario

The developers actually left a comment in the code acknowledging this rounding behavior, noting that it "temporarily increases the target position's LTV until the position is fully migrated."

However, this assumption is fatally flawed. An attacker can weaponize this "temporary" state:

  1. The Setup: A borrower creates a SELL offer to migrate a large position from Market A to Market B. The borrower also has some pre-existing collateral on Market B.
  2. The Dust Fill: An attacker (or a malicious bot) intentionally fills a tiny fraction of that offer (e.g., 1 wei).
  3. The Rounding: The contract repays 1 wei of debt on Market A, but transfers 0 collateral to Market B.
  4. The Kill: Market B now records a new debt for the borrower, but 0 new collateral backed it. The LTV on Market B is instantly pushed to infinity.
  5. The Liquidation: The attacker immediately liquidates the borrower on Market B, stealing the liquidation penalty and the borrower's pre-existing collateral.

The Proof of Concept (Foundry)

To prove this, I wrote a Foundry test that replicates the math. I mocked the Midnight interface and set up a scenario where a user has 1000e18 collateral but 1000e18 + 1 debt (simulating accrued interest).

// SPDX-License-Identifier: MIT
pragma solidity 0.8.34;

import {Test, console} from "forge-std/Test.sol";
import {CollateralTransferLib} from "../src/libraries/CollateralTransferLib.sol";
import {Market, CollateralParams, IMidnight} from "@midnight/interfaces/IMidnight.sol";

contract DustFillExploit is Test {
    // ... Mocks setup ...

    function testDustFillRoundsCollateralToZero() public {
        // 1. Borrower has 1000e18 collateral on Market A
        midnight.setCollateral(sourceMarketId, borrower, 0, 1000e18);

        // 2. Debt is 1000e18 + 1 (due to accrued interest)
        uint128 sourceDebtBefore = 1000e18 + 1;

        // 3. Attacker does a dust fill of 1 wei
        uint256 repaidUnits = 1;

        // 4. Call the vulnerable library function directly
        (address[] memory tokens, uint256[] memory amounts) = CollateralTransferLib.transferCollaterals(
            IMidnight(address(midnight)), 
            sourceMarket,
            targetMarket,
            borrower,
            sourceMarketId,
            sourceDebtBefore,
            repaidUnits
        );

        console.log("Collateral transferred to target:", amounts[0]);

        // 5. The transferred collateral will be 0, proving the vulnerability
        assertEq(amounts[0], 0, "Vulnerability proven: collateral rounded to zero!");
    }
}

Output:

[PASS] testDustFillRoundsCollateralToZero() (gas: 65335)
Logs:
  Collateral transferred to target: 0

How to Fix It

To mitigate this vulnerability, the transferCollaterals function should enforce a minimum transfer amount. If the pro-rata calculation rounds down to zero, the transaction should revert to prevent unbacked debt from being created.

if (sourceDebtBefore != repaidUnits) {
    collateralToTransfer = collateralToTransfer.mulDivDown(repaidUnits, sourceDebtBefore);
    // Add this check:
    if (collateralToTransfer == 0 && repaidUnits > 0) {
        revert CallbackLib.ZeroAmount();
    }
}

Conclusion

This bug is a perfect example of why rounding directions and edge cases matter so much in DeFi. A simple mulDivDown combined with accrued interest created a path to drain user funds.

If you are a developer, always assume debt and balances will have dust amounts, and protect your math accordingly. If you are a researcher, always read the developer commentsβ€”they often point you directly to the attack surface!

You can check out more of my security research and PoCs on my GitHub Portfolio.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.