<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: vault vult</title>
    <description>The latest articles on DEV Community by vault vult (@vaultvult).</description>
    <link>https://dev.to/vaultvult</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4205582%2F55b6ce90-72f5-4758-b2c9-29f2276bf714.jpg</url>
      <title>DEV Community: vault vult</title>
      <link>https://dev.to/vaultvult</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vaultvult"/>
    <language>en</language>
    <item>
      <title>How I Found a High-Severity Rounding Vulnerability in Morpho Midnight (Tenor Labs)</title>
      <dc:creator>vault vult</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:52:44 +0000</pubDate>
      <link>https://dev.to/vaultvult/how-i-found-a-high-severity-rounding-vulnerability-in-morpho-midnight-tenor-labs-35c2</link>
      <guid>https://dev.to/vaultvult/how-i-found-a-high-severity-rounding-vulnerability-in-morpho-midnight-tenor-labs-35c2</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Tenor Labs &amp;amp; Morpho Midnight
&lt;/h2&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;This is handled by &lt;code&gt;BorrowMidnightRenewalCallback.sol&lt;/code&gt; and the &lt;code&gt;CollateralTransferLib&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vulnerability: &lt;code&gt;mulDivDown&lt;/code&gt; Rounding to Zero
&lt;/h2&gt;

&lt;p&gt;The core of the bug lies in the &lt;code&gt;transferCollaterals&lt;/code&gt; function within &lt;code&gt;CollateralTransferLib.sol&lt;/code&gt;. The code uses &lt;code&gt;mulDivDown&lt;/code&gt; to calculate the collateral to transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (sourceDebtBefore != repaidUnits) {
    collateralToTransfer = collateralToTransfer.mulDivDown(repaidUnits, sourceDebtBefore);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mulDivDown&lt;/code&gt; performs multiplication followed by division, &lt;strong&gt;always rounding down&lt;/strong&gt;. &lt;/p&gt;

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

&lt;p&gt;If an attacker performs a "dust fill" on the migration offer (repaying exactly &lt;strong&gt;1 wei&lt;/strong&gt;), the math looks like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;1000e18 * 1 / (1000e18 + 1) = 0.999...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;mulDivDown&lt;/code&gt; rounds down to the nearest integer, the &lt;code&gt;collateralToTransfer&lt;/code&gt; becomes &lt;strong&gt;0&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Exploit Scenario
&lt;/h2&gt;

&lt;p&gt;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." &lt;/p&gt;

&lt;p&gt;However, this assumption is fatally flawed. An attacker can weaponize this "temporary" state:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Setup:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Dust Fill:&lt;/strong&gt; An attacker (or a malicious bot) intentionally fills a tiny fraction of that offer (e.g., 1 wei).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Rounding:&lt;/strong&gt; The contract repays 1 wei of debt on Market A, but transfers &lt;strong&gt;0 collateral&lt;/strong&gt; to Market B.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Kill:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Liquidation:&lt;/strong&gt; The attacker immediately liquidates the borrower on Market B, stealing the liquidation penalty and the borrower's pre-existing collateral.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Proof of Concept (Foundry)
&lt;/h2&gt;

&lt;p&gt;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).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// 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!");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[PASS] testDustFillRoundsCollateralToZero() (gas: 65335)
Logs:
  Collateral transferred to target: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Fix It
&lt;/h2&gt;

&lt;p&gt;To mitigate this vulnerability, the &lt;code&gt;transferCollaterals&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (sourceDebtBefore != repaidUnits) {
    collateralToTransfer = collateralToTransfer.mulDivDown(repaidUnits, sourceDebtBefore);
    // Add this check:
    if (collateralToTransfer == 0 &amp;amp;&amp;amp; repaidUnits &amp;gt; 0) {
        revert CallbackLib.ZeroAmount();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

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

&lt;p&gt;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!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You can check out more of my security research and PoCs on my &lt;a href="https://github.com/vaultvult" rel="noopener noreferrer"&gt;GitHub Portfolio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>security</category>
      <category>solidity</category>
      <category>smartcontracts</category>
    </item>
  </channel>
</rss>
