Summary
The credit delegation system fails catastrophically when a market's new credit delegation value is less than its previous value. This occurs because Vault::_updateCreditDelegations
incorrectly handles decreases in credit delegation using unsigned arithmetic (UD60x18), causing underflows that break core protocol operations like deposits and redemptions.
Vulnerability Details
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/src/market-making/leaves/Vault.sol#L601C1-L605C70
The _updateCreditDelegations function calculates credit delegation changes using unsigned arithmetic:
UD60x18 creditDeltaUsdX18 = newCreditDelegationUsdX18.sub(previousCreditDelegationUsdX18);
However, Market.sol
has two distinct functions for handling credit updates:
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/src/market-making/leaves/Market.sol#L492C1-L495C6
https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/src/market-making/leaves/Market.sol#L471C1-L473C6
function updateTotalDelegatedCredit(Data storage self, UD60x18 creditDeltaUsdX18)
function updateTotalDelegatedCredit(Data storage self, SD59x18 creditDeltaUsdX18)
Here's an example to showcase the issue without checking if negative delta and always using UD60x18:
newCreditDelegationUsdX18 = 316800000
previousCreditDelegationUsdX18 = 396000000
Impact
This breaks critical functions that depend on recalculateVaultsCreditCapacity, including:
Deposits failing
Redemptions failing
Credit capacity updates
Market debt calculations
Tools Used
Foundry
Recommendations
Inside of Vault::_updateCreditDelegations
create an If statement logic to handle if it's going to be a negative delta to handle proper assignment of SD59x18
or UD60x18
and call the right Market::updateTotalDelegatedCredit
this way.
function _updateCreditDelegations(
...
)
private
returns (...)
{
.... start of function
+ bool isNegativeDelta = newCreditDelegationUsdX18.lt(previousCreditDelegationUsdX18);
+ Market.Data storage market = Market.load(connectedMarketId);
+ if (isNegativeDelta) {
+ SD59x18 creditDeltaUsdX18 = newCreditDelegationUsdX18.intoSD59x18().sub(previousCreditDelegationUsdX18.intoSD59x18());
+ market.updateTotalDelegatedCredit(creditDeltaUsdX18);
+ } else {
+ UD60x18 creditDeltaUsdX18 = newCreditDelegationUsdX18.sub(previousCreditDelegationUsdX18);
+ market.updateTotalDelegatedCredit(creditDeltaUsdX18);
}
- UD60x18 creditDeltaUsdX18 = newCreditDelegationUsdX18.sub(previousCreditDelegationUsdX18);
- // loads the market's storage pointer and update total delegated credit
- Market.Data storage market = Market.load(connectedMarketId);
- market.updateTotalDelegatedCredit(creditDeltaUsdX18);
... rest of function
}