- Severity: 🟡 Medium — Score: 48 — Priority: P1
- Category: Liquidity risk / external protocol integration
- Location:
_byzantineLiquid(), maxWithdraw(), maxRedeem(), harvest()
- Confidence: High
- Source: @crypto4all audit
Description
_byzantineLiquid() values the Byzantine position via convertToAssets — an accounting valuation, not a guarantee of available liquidity:
function _byzantineLiquid() internal view returns (uint256) {
uint256 idle = IERC20(asset()).balanceOf(address(this));
uint256 byzShares = BYZANTINE_VAULT.balanceOf(address(this));
uint256 byzAssets = byzShares > 0
? BYZANTINE_VAULT.convertToAssets(byzShares) : 0;
return idle + byzAssets;
}
Yet _withdraw attempts a real withdrawal:
if (idle < assets) {
BYZANTINE_VAULT.withdraw(assets - idle, address(this), address(this));
}
Under Morpho liquidity stress (over-utilized markets, withdrawal cascade), BYZANTINE_VAULT.maxWithdraw() drops well below convertToAssets(). Result: maxWithdraw on the PacktolVault side promises available funds, but the actual withdraw call reverts. The code comment even admits bypassing Byzantine's maxWithdraw() — amplifying the divergence.
Mitigating Factors
The situation is self-resolving once Morpho liquidity is restored. Funds are not lost, only temporarily inaccessible.
Recommendation 1
Introduce a dedicated function for real liquidity, distinct from the accounting valuation:
function _byzantineWithdrawable() internal view returns (uint256) {
uint256 shares = BYZANTINE_VAULT.balanceOf(address(this));
if (shares == 0) return 0;
uint256 theoretical = BYZANTINE_VAULT.convertToAssets(shares);
uint256 withdrawable = BYZANTINE_VAULT.maxWithdraw(address(this));
return withdrawable < theoretical ? withdrawable : theoretical;
}
Recommendation 2
Use _byzantineWithdrawable() in maxWithdraw / maxRedeem. Retain convertToAssets exclusively in totalAssets() (accounting value), never as a liquidity proxy.
_byzantineLiquid(),maxWithdraw(),maxRedeem(),harvest()Description
_byzantineLiquid()values the Byzantine position viaconvertToAssets— an accounting valuation, not a guarantee of available liquidity:Yet
_withdrawattempts a real withdrawal:Under Morpho liquidity stress (over-utilized markets, withdrawal cascade),
BYZANTINE_VAULT.maxWithdraw()drops well belowconvertToAssets(). Result:maxWithdrawon the PacktolVault side promises available funds, but the actualwithdrawcall reverts. The code comment even admits bypassing Byzantine'smaxWithdraw()— amplifying the divergence.Mitigating Factors
The situation is self-resolving once Morpho liquidity is restored. Funds are not lost, only temporarily inaccessible.
Recommendation 1
Introduce a dedicated function for real liquidity, distinct from the accounting valuation:
Recommendation 2
Use
_byzantineWithdrawable()inmaxWithdraw/maxRedeem. RetainconvertToAssetsexclusively intotalAssets()(accounting value), never as a liquidity proxy.