Summary
A critical vulnerability exists in the Vault::updateVaultAndCreditDelegationWeight, where the weight assigned to each connected market is incorrectly set to the total assets of the vault instead of a proportional share. This results in the total delegated credit across all markets exceeding the vault's total assets, leading to overleveraging and potential insolvency.
Vulnerability Details
Vault::updateVaultAndCreditDelegationWeight is called in Vault::Vault::recalculateVaultsCreditCapacity
and its job is to set weights of each connected market proportional to the total weight of the vault. See function below:
function updateVaultAndCreditDelegationWeight(
Data storage self,
uint128[] memory connectedMarketsIdsCache
)
internal
{
uint256 connectedMarketsConfigLength = self.connectedMarkets.length;
EnumerableSet.UintSet storage connectedMarkets = self.connectedMarkets[connectedMarketsConfigLength - 1];
uint128 newWeight = uint128(IERC4626(self.indexToken).totalAssets());
for (uint256 i; i < connectedMarketsIdsCache.length; i++) {
CreditDelegation.Data storage creditDelegation =
CreditDelegation.load(self.id, connectedMarkets.at(i).toUint128());
creditDelegation.weight = newWeight;
}
self.totalCreditDelegationWeight = newWeight;
}
The function assigns the total assets of the vault (newWeight) to each connected market individually:
creditDelegation.weight = newWeight;
If there are multiple connected markets, each market is assigned the full weight of the vault, leading to an overestimation of the total delegated credit.
Example:
Vault Total Assets = 1000 wETH
Connected Markets = 2
Each market is assigned a weight of 1000 wETH.
Total delegated credit = 1000 + 1000 = 2000wETH (exceeds the vault's total assets).
Impact
Overleveraging: The total delegated credit across all markets exceeds the vault's total assets, putting the system in an overleveraged and unsafe state.
Financial Instability: If one or more markets face losses, the vault may not have enough assets to cover the losses, leading to insolvency.
Exploitation Risk: Malicious actors could exploit this bug to overleverage the system, resulting in significant financial losses.
Loss of User Funds: Users who deposit collateral into the vault may lose their funds if the vault becomes insolvent due to overleveraging.
Proof Of Code (POC)
I ran the following POC to prove the exploit:
function testFuzz_marketweightsharescalculationerror(
uint256 vaultId1,
uint256 marketId1,
uint256 vaultId2,
uint256 marketId2
)
external
{
vm.stopPrank();
VaultConfig memory fuzzVaultConfig1 = getFuzzVaultConfig(vaultId1);
vm.assume(fuzzVaultConfig1.asset != address(usdc));
PerpMarketCreditConfig memory fuzzMarketConfig1 = getFuzzPerpMarketCreditConfig(marketId1);
VaultConfig memory fuzzVaultConfig2 = getFuzzVaultConfig(vaultId2);
vm.assume(fuzzVaultConfig2.asset != address(usdc));
PerpMarketCreditConfig memory fuzzMarketConfig2 = getFuzzPerpMarketCreditConfig(marketId2);
vm.assume(fuzzMarketConfig1.marketId != fuzzMarketConfig2.marketId);
vm.assume(fuzzVaultConfig1.vaultId != fuzzVaultConfig2.vaultId);
uint256[] memory marketIds = new uint256[](2);
marketIds[0] = fuzzMarketConfig1.marketId;
marketIds[1] = fuzzMarketConfig2.marketId;
uint256[] memory vaultIds = new uint256[](2);
vaultIds[0] = fuzzVaultConfig1.vaultId;
vaultIds[1] = fuzzVaultConfig2.vaultId;
vm.prank(users.owner.account);
marketMakingEngine.connectVaultsAndMarkets(marketIds, vaultIds);
address user = users.naruto.account;
deal(fuzzVaultConfig1.asset, user, 100e18);
deal(fuzzVaultConfig2.asset, user, 100e18);
vm.startPrank(user);
marketMakingEngine.deposit(fuzzVaultConfig1.vaultId, 1e18, 0, "", false);
marketMakingEngine.deposit(fuzzVaultConfig1.vaultId, 1e18, 0, "", false);
marketMakingEngine.deposit(fuzzVaultConfig2.vaultId, 1e18, 0, "", false);
marketMakingEngine.deposit(fuzzVaultConfig2.vaultId, 1e18, 0, "", false);
vm.stopPrank();
deal({ token: address(fuzzVaultConfig1.asset), to: address(fuzzMarketConfig1.engine), give: 10e18 });
deal({ token: address(fuzzVaultConfig2.asset), to: address(fuzzMarketConfig2.engine), give: 10e18 });
vm.prank( address(fuzzMarketConfig1.engine));
marketMakingEngine.depositCreditForMarket(fuzzMarketConfig1.marketId, fuzzVaultConfig1.asset, 2e18);
vm.prank( address(fuzzMarketConfig2.engine));
marketMakingEngine.depositCreditForMarket(fuzzMarketConfig2.marketId, fuzzVaultConfig2.asset, 2e18);
uint256 totalmarketdebt;
for(uint i = 0; i < marketIds.length; i++){
SD59x18 marketdebt = marketMakingEngine.workaround_getTotalMarketDebt(uint128(marketIds[i]));
console.log(marketdebt.unwrap());
totalmarketdebt += uint256(marketdebt.unwrap());
}
console.log(totalmarketdebt);
function getTotalAssetsUsd(Data storage self) internal view returns (UD60x18 totalAssetsUsdX18) {
Collateral.Data storage collateral = self.collateral;
UD60x18 totalAssetsX18 = ud60x18(IERC4626(self.indexToken).totalAssets());
totalAssetsUsdX18 = collateral.getAdjustedPrice().mul(totalAssetsX18);
I also included this function in VaultRouterBranch.sol:
function gettotalassetsusd(uint128 vaultId) external view returns (uint256) {
Vault.Data storage vault = Vault.loadLive(vaultId);
return vault.getTotalAssetsUsd().intoUint256();
}
After registering the selector of this function in TreeProxyUtils.sol, it should work as expected
*/
uint256 totalassetvalue;
for (uint i = 0; i< vaultIds.length; i++){
uint256 assetvalueusd = marketMakingEngine.gettotalassetsusd(uint128(vaultIds[i]));
totalassetvalue += assetvalueusd;
}
console.log(totalassetvalue);
assert(totalmarketdebt > totalassetvalue);
}
Tools Used
Manual Review, Foundry
Recommendations
Update Vault::updateVaultAndCreditDelegationWeight function to assign a proportional share of the vault's total assets to each connected market. This can be achieved by dividing the vault's total assets by the number of connected markets.
function updateVaultAndCreditDelegationWeight(
Data storage self,
uint128[] memory connectedMarketsIdsCache
)
internal
{
uint256 connectedMarketsConfigLength = self.connectedMarkets.length;
EnumerableSet.UintSet storage connectedMarkets = self.connectedMarkets[connectedMarketsConfigLength - 1];
uint128 newWeight = uint128(IERC4626(self.indexToken).totalAssets());
uint128 weightPerMarket = newWeight / uint128(connectedMarketsIdsCache.length);
for (uint256 i; i < connectedMarketsIdsCache.length; i++) {
CreditDelegation.Data storage creditDelegation =
CreditDelegation.load(self.id, connectedMarkets.at(i).toUint128());
creditDelegation.weight = weightPerMarket;
}
self.totalCreditDelegationWeight = newWeight;
}
Add a check to ensure that the number of connected markets is greater than zero before dividing.
require(connectedMarketsIdsCache.length > 0, "No connected markets");