2025-04-27
*issue*
================================================================================

Defaulted Idle CV with pending withdrawals will permanently break stablecoin accounting

================================================================================

*issue-contents*

0 CONTENTS

*issue-metadata*

1 METADATA

Number 148
Severity Medium
Author 0x52
Contest Pareto USP
Platform Sherlock
*issue-summary*

2 SUMMARY

IdleCDOEpochVariant.sol#L646-L652

/// @notice Claim a withdraw request from the vault. Can be done when at least 1 epoch passed
/// since last withdraw request
function claimWithdrawRequest() external {
    // underlyings requested, here we check that user waited at least one epoch and that borrower
    // did not default upon repayment (old requests can still be claimed)
    IdleCreditVault(strategy).claimWithdrawRequest(msg.sender);
}

When a borrower defaults after failing to pay interest or pending withdraws, the pending withdrawals of the previous epoch become unclaimable as the IdleCreditVault does not allows withdrawals to be claimed on the defaulted epoch.

ParetoDollarQueue.sol#L183-L193

      function scaledNAVCreditVault(address yieldSource, address vaultToken, IERC20Metadata token) internal view returns (uint256) {
        IIdleCDOEpochVariant cv = IIdleCDOEpochVariant(yieldSource);
        IIdleCreditVault strategy = IIdleCreditVault(cv.strategy());

        uint256 decimals = token.decimals();
    @>  uint256 pending = strategy.withdrawsRequests(address(this)) * 10 ** (18 - decimals);
        uint256 instantPending = strategy.instantWithdrawsRequests(address(this)) * 10 ** (18 - decimals);
        // tranche balance in this contract (which have 18 decimals) * price (in underlying decimals) / 10 ** underlying decimals
        // we also need to add eventual pending withdraw requests (both normal and instant) as these requests burn tranche tokens
    @>  return IERC20Metadata(vaultToken).balanceOf(address(this)) * cv.virtualPrice(cv.AATranche()) / (10 ** decimals) + pending + instantPending;
      }

As a result of this, scaledNAVCreditVault will forever account for these assets as real assets even though they are unclaimable, erroneously inflating the NAV of the stablecoin.

ParetoDollarQueue.sol#L605-L633

      function removeYieldSource(address _source) external {
        _checkOwner();

        YieldSource memory _ys = yieldSources[_source];
        // revert if the token is not in the yield sources
        if (address(_ys.source) == address(0)) {
          revert YieldSourceInvalid();
        }
        // revert if the yield source is not empty, no need to unscale the value
    @>  if (getCollateralsYieldSourceScaled(_source) > 0) {
          revert YieldSourceNotEmpty();
        }
        // remove allowance for the yield source
        _ys.token.safeDecreaseAllowance(_source, _ys.token.allowance(address(this), _source));
        // remove the yield source from mapping
        delete yieldSources[_source];
        // remove the source from the list of all yield sources
        // order is not preserved but it's not important (last el can be reallocated)
        YieldSource[] memory _sources = allYieldSources;
        uint256 sourcesLen = _sources.length;
        for (uint256 i = 0; i < sourcesLen; i++) {
          if (address(_sources[i].source) == address(_ys.source)) {
            allYieldSources[i] = _sources[sourcesLen - 1];
            allYieldSources.pop();
            break;
          }
        }
        emit YieldSourceRemoved(_source);
      }

In addition to the phantom value that will inflate the assets of the USP and cause unbacked USP to be minted, the defaulted vault will be impossible to remove due to the check ensuring that the yield source is empty. With other assets, the owner could simply remove the tokens using the emergencyUtils but in this case since the pending is hard coded. The result is that the defaulted credit vault and it’s unclaimable pending withdraws will be permanently stuck on the balance sheet.

*issue-root-cause*

3 ROOT CAUSE

Same epoch pending withdrawals are unclaimable upon CV default

ParetoDollarQueue.sol#L188 hard codes pending withdrawals

ParetoDollarQueue.sol#L614-L616 cannot be bypassed and reverts if NAV != 0

*issue-internal-pre-conditions*

4 INTERNAL PRE-CONDITIONS

None

*issue-external-pre-conditions*

5 EXTERNAL PRE-CONDITIONS

Idle CV defaults

*issue-attack-path*

6 ATTACK PATH

N/A

*issue-impact*

7 IMPACT

Erroneous value is permanently stuck on USP balance sheet causing unbacked USP to be minted

*issue-poc*

8 POC

N/A

*issue-mitigation*

9 MITIGATION

The check in ParetoDollarQueue.sol#L614-L616 should be bypassed if the CV being removed has defaulted.

================================================================================

LINKS

*issue-links*