Defaulted Idle CV with pending withdrawals will permanently break stablecoin accounting
*issue-contents*
0 CONTENTS
- 1................................................................................
- 2................................................................................
- 3................................................................................
- 4................................................................................
- 5................................................................................
- 6................................................................................
- 7................................................................................
- 8................................................................................
- 9................................................................................
1 METADATA
Number | 148 |
---|---|
Severity | Medium |
Author | 0x52 |
Contest | Pareto USP |
Platform | Sherlock |
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.
3 ROOT CAUSE
Same epoch pending withdrawals are unclaimable upon CV default
hard codes pending withdrawals ParetoDollarQueue.sol#L188
cannot be bypassed and reverts if NAV != 0 ParetoDollarQueue.sol#L614-L616
4 INTERNAL PRE-CONDITIONS
None
5 EXTERNAL PRE-CONDITIONS
Idle CV defaults
6 ATTACK PATH
N/A
7 IMPACT
Erroneous value is permanently stuck on USP balance sheet causing unbacked USP to be minted
8 POC
N/A
9 MITIGATION
The check in should be bypassed if the CV being removed has defaulted. ParetoDollarQueue.sol#L614-L616
LINKS
*issue-links*
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.