Summary
The realized debt returned by getRealizedDebtUsd
is incorrectly calculated. This information is used all over the project, breaking the countability
Vulnerability Details
Upon closing a profitable position trading engine calls creditDelegationBranch::withdrawUsdTokenFromMarket
to reward the trader.
This function will mint the required usdToken
and increase the market's netUsdTokenIssuance
by the same amount.
function withdrawUsdTokenFromMarket(uint128 marketId, uint256 amount) external onlyRegisteredEngine(marketId) {
...
market.updateNetUsdTokenIssuance(adjustedUsdTokenToMintX18.intoSD59x18());
} else {
@> market.updateNetUsdTokenIssuance(amountX18.intoSD59x18());
}
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
UsdToken usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[msg.sender]);
usdToken.mint(msg.sender, amountToMint);
...
The netUsdTokenIssuance
represents the net amount of usdToken
minted and burned by respective market. This amount is seen as a debt for the market.
On the other hand when a trader is liquidated (or settles a position with negative PnL) collateral is transferred from trading engine
to market-making engine
via CreditDelegationBranch::depositCreditForMarket
.
function depositCreditForMarket(
uint128 marketId,
address collateralAddr,
uint256 amount
)
external
onlyRegisteredEngine(marketId)
{
if (collateralAddr == usdToken) {
@> market.updateNetUsdTokenIssuance(unary(amountX18.intoSD59x18()));
} else {
if (collateralAddr == usdc) {
market.settleCreditDeposit(address(0), amountX18);
} else {
@> market.depositCredit(collateralAddr, amountX18);
}
}
IERC20(collateralAddr).safeTransferFrom(msg.sender, address(this), amount);
If collateral used is usdToken
the market's netUsdTokenIssuance
is decreased (see the unary
operation), reducing the market's realized debt.
If collateral is USDC, the amount of usdc to be distributed to vaults is updated and
if it's another collateral, it's added to creditDeposits
. These deposits represents the market's profit.
function depositCredit(Data storage self, address asset, UD60x18 amountX18) internal {
AssetToAmountMap.update(self.creditDeposits, asset, amountX18, true);
}
The problem is that Market::getRealizedDebtUsd
returns the sum of creditDeposits
and netUsdTokenIssuance
as realized debt.
Instead it should subtract credit deposits
from netUsdTokenIssuance
.
function getRealizedDebtUsd(Data storage self) internal view returns (SD59x18 realizedDebtUsdX18) {
UD60x18 creditDepositsValueUsdX18;
if (block.timestamp <= self.lastCreditDepositsValueRehydration) {
creditDepositsValueUsdX18 = ud60x18(self.creditDepositsValueCacheUsd);
} else {
@> creditDepositsValueUsdX18 = getCreditDepositsValueUsd(self);
}
@> realizedDebtUsdX18 = creditDepositsValueUsdX18.intoSD59x18().add(sd59x18(self.netUsdTokenIssuance));
}
The returned value is used many places
Impact
The getCreditDepositsValueUsd
returned values is used in many places in the codebase, breaking the market-making engine accounting.
Tools Used
Recommendations
function getRealizedDebtUsd(Data storage self) internal view returns (SD59x18 realizedDebtUsdX18) {
// prepare the credit deposits usd value variable;
UD60x18 creditDepositsValueUsdX18;
// if the credit deposits usd value cache is up to date, return the stored value
if (block.timestamp <= self.lastCreditDepositsValueRehydration) {
creditDepositsValueUsdX18 = ud60x18(self.creditDepositsValueCacheUsd);
} else {
// otherwise, we'll need to loop over credit deposits to calculate it
creditDepositsValueUsdX18 = getCreditDepositsValueUsd(self);
}
// finally after determining the market's latest credit deposits usd value, sum it with the stored net usd
// token issuance to return the net realized debt usd value
- realizedDebtUsdX18 = creditDepositsValueUsdX18.intoSD59x18().add(sd59x18(self.netUsdTokenIssuance));
+ realizedDebtUsdX18 = sd59x18(self.netUsdTokenIssuance).sub(creditDepositsValueUsdX18.intoSD59x18());
}