Skip to content
Request an audit

‹ All findings

Cannot repay or liquidate on paused asset

MediumCap·Sherlock · Stablecoin issuer · 10th July 2025CodebaseM-4

Summary

Although the protocol allows repayment on paused asset, it cannot be done because Lender contract tries an unnecessary (and prohibited due to pause) borrow from Vault contract.

Root Cause

First, let's see why repayment on paused asset is an intended protocol design.

So for the paused assets, the protocol wants to:

  • Disallow minting or borrowing
  • Allow repaying or liquidating
  • Avoid/delay collecting interest

However, repayment on paused assets (and consequently liquidations) will not be processed because Lender contract unncessarily tries to borrow from Vault contract:

File: cap-contracts/contracts/lendingPool/libraries/BorrowLogic.sol

solidity
201:    function realizeRestakerInterest(ILender.LenderStorage storage $, address _agent, address _asset)
202:         public
203:         returns (uint256 realizedInterest)
204:     {
205:         ILender.ReserveData storage reserve = $.reservesData[_asset];
206:         uint256 unrealizedInterest;
207:         (realizedInterest, unrealizedInterest) = maxRestakerRealization($, _agent, _asset);
208:         reserve.lastRealizationTime[_agent] = block.timestamp;
209: 
210:         if (realizedInterest == 0 && unrealizedInterest == 0) return 0;
211: 
212:         reserve.debt += realizedInterest;
213:         reserve.unrealizedInterest[_agent] += unrealizedInterest;
214:         reserve.totalUnrealizedInterest += unrealizedInterest;
215: 
216:         IDebtToken(reserve.debtToken).mint(_agent, realizedInterest + unrealizedInterest);
217:@>       IVault(reserve.vault).borrow(_asset, realizedInterest, $.delegation);
218:         IDelegation($.delegation).distributeRewards(_agent, _asset);
219:         emit RealizeInterest(_asset, realizedInterest, $.delegation);
220:     }

Internal Pre-conditions

An asset is paused

External Pre-conditions

n/a

Attack Path

n/a

Impact

The protocol cannot get debt repaid from agents on paused assets.

PoC

solidity
pragma solidity ^0.8.28;

import { IOracle } from "../contracts/interfaces/IOracle.sol";
import { TestDeployer } from "./deploy/TestDeployer.sol";
import { console } from "forge-std/console.sol";

contract POC is TestDeployer {
    address user_agent;

    function setUp() public {
        _deployCapTestEnvironment();

        _initTestVaultLiquidity(usdVault);
        _initSymbioticVaultsLiquidity(env);

        user_agent = _getRandomAgent();

        vm.startPrank(env.symbiotic.users.vault_admin);
        _symbioticVaultDelegateToAgent(symbioticWethVault, env.symbiotic.networkAdapter, user_agent, 100e18);
        vm.stopPrank();
    }

    function test_submissionValidity() public {
        _setAssetOraclePrice(address(weth), 2000e8);
        vm.startPrank(user_agent);
        lender.borrow(address(usdc), 11000e6, user_agent);
        vm.stopPrank();

        vm.startPrank(env.users.access_control_admin);
        accessControl.grantAccess(lender.pauseAsset.selector, address(lender), env.users.vault_config_admin);
        vm.stopPrank();

        vm.startPrank(env.users.vault_config_admin);
        cUSD.pauseAsset(address(usdc));
        lender.pauseAsset(address(usdc), true);
        vm.stopPrank();

        vm.startPrank(user_agent);
        _timeTravel(90 days);
        usdc.approve(address(lender), 5000e6);

        vm.expectRevert(abi.encodeWithSignature("AssetPaused(address)", address(usdc)));
        lender.repay(address(usdc), 5000e6, user_agent);
        vm.stopPrank();
    }
}

Mitigation

diff
diff --git a/cap-contracts/contracts/lendingPool/libraries/BorrowLogic.sol b/cap-contracts/contracts/lendingPool/libraries/BorrowLogic.sol
index 3c2b60a..e36f043 100644
--- a/cap-contracts/contracts/lendingPool/libraries/BorrowLogic.sol
+++ b/cap-contracts/contracts/lendingPool/libraries/BorrowLogic.sol
@@ -214,8 +214,10 @@ library BorrowLogic {
         reserve.totalUnrealizedInterest += unrealizedInterest;
 
         IDebtToken(reserve.debtToken).mint(_agent, realizedInterest + unrealizedInterest);
-        IVault(reserve.vault).borrow(_asset, realizedInterest, $.delegation);
-        IDelegation($.delegation).distributeRewards(_agent, _asset);
+        if (realizedInterest > 0) {
+            IVault(reserve.vault).borrow(_asset, realizedInterest, $.delegation);
+            IDelegation($.delegation).distributeRewards(_agent, _asset);
+        }
         emit RealizeInterest(_asset, realizedInterest, $.delegation);
     }
 

After applying the patch, run the POC again to see if repayment on paused asset is possible.