Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Valid

Blacklisted Recipient Can Break functions that call distributeProtocolAssetReward Function

Summary

The distributeProtocolAssetReward function distributes protocol rewards among multiple fee recipients based on their share percentages. The function iterates over the list of recipients, calculates their respective rewards, and transfers the asset (USDC in this case) to each recipient.

However, if any recipient is blacklisted (e.g., by USDC’s centralized contract), the transfer will fail, causing the function to revert and preventing all recipients from receiving their rewards.

Vulnerability Details

function distributeProtocolAssetReward(Data storage self, address asset, uint256 amount) internal {
// cache unchanging variables before loop
uint256 feeRecipientsLength = self.protocolFeeRecipients.length();
UD60x18 totalFeeRecipientsSharesX18 = ud60x18(self.totalFeeRecipientsShares);
UD60x18 amountX18 = ud60x18(amount);
// variable to verify the total distributed
uint256 totalDistributed = 0;
// iterate over the protocol configured fee recipients
for (uint256 i; i < feeRecipientsLength; i++) {
// load the fee recipient address and shares
(address feeRecipient, uint256 shares) = self.protocolFeeRecipients.at(i);
// Calculate the fee recipient reward
uint256 feeRecipientReward = amountX18.mul(ud60x18(shares)).div(totalFeeRecipientsSharesX18).intoUint256();
// cache the total distributed
totalDistributed += feeRecipientReward;
// verify if is the last fee recipient
if (i == feeRecipientsLength - 1) {
// to prevent small amounts of protocol fees remain stuck in the contract due to rounding
feeRecipientReward += amountX18.sub(ud60x18(totalDistributed)).intoUint256();
}
// Transfer the fee recipient reward
IERC20(asset).safeTransfer(feeRecipient, feeRecipientReward);
}
}

One of the functions that use distributeProtocolAssetReward

function _convertUsdcToAssets(
uint128 dexSwapStrategyId,
address asset,
uint256 usdcAmount,
bytes memory path,
address recipient,
address usdc
)
internal
returns (uint256 assetOut)
{
// revert if the amount is zero
if (usdcAmount == 0) revert Errors.AssetAmountIsZero(usdc);
// if the asset being handled is usdc, output it to `usdcOut`
if (asset == usdc) {
assetOut = usdcAmount;
} else {
// load the market making engine configuration storage pointer
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// cache the settlement base fee value using usdc's native decimals
uint256 settlementBaseFeeUsd = Collateral.load(usdc).convertUd60x18ToTokenAmount(
ud60x18(marketMakingEngineConfiguration.settlementBaseFeeUsdX18)
);
if (settlementBaseFeeUsd > 0) {
// revert if there isn't enough usdc to convert the base fee
// NOTE: keepers must be configured to buy good chunks of usdc at minimum (e.g $500)
// as the settlement base fee shouldn't be much greater than $1.
if (usdcAmount < settlementBaseFeeUsd) {
revert Errors.FailedToPaySettlementBaseFee();
}
// subtract fee from usdc input
usdcAmount -= settlementBaseFeeUsd;
// distribute the base fee to protocol fee recipients
@> marketMakingEngineConfiguration.distributeProtocolAssetReward(usdc, settlementBaseFeeUsd);
}
// loads the dex swap strategy data storage pointer
DexSwapStrategy.Data storage dexSwapStrategy = DexSwapStrategy.loadExisting(dexSwapStrategyId);
// approve the asset to be spent by the dex adapter contract
IERC20(usdc).approve(dexSwapStrategy.dexAdapter, usdcAmount);
// verify if the swap should be input single or multihop
if (path.length == 0) {
// prepare the data for executing the swap
SwapExactInputSinglePayload memory swapCallData = SwapExactInputSinglePayload({
tokenIn: usdc,
tokenOut: asset,
amountIn: usdcAmount,
recipient: recipient
});
// swap the credit deposit assets for USDC and store the output amount
assetOut = dexSwapStrategy.executeSwapExactInputSingle(swapCallData);
} else {
// prepare the data for executing the swap
SwapExactInputPayload memory swapCallData = SwapExactInputPayload({
path: path,
tokenIn: usdc,
tokenOut: asset,
amountIn: usdcAmount,
recipient: recipient
});
// swap the credit deposit assets for USDC and store the output amount
assetOut = dexSwapStrategy.executeSwapExactInput(swapCallData);
}
}
}
function convertMarketsCreditDepositsToUsdc(
uint128 marketId,
address[] calldata assets,
uint128[] calldata dexSwapStrategyIds,
bytes[] calldata paths
)
external
onlyRegisteredSystemKeepers
{
// revert if the arrays have different lengths
if (assets.length != dexSwapStrategyIds.length || assets.length != paths.length) {
// we ignore in purpose the error params here
revert Errors.ArrayLengthMismatch(0, 0);
}
// load the market's data storage pointer
Market.Data storage market = Market.loadExisting(marketId);
// working area
ConvertMarketsCreditDepositsToUsdcContext memory ctx;
for (uint256 i; i < assets.length; i++) {
// revert if the market hasn't received any fees for the given asset
(bool exists, uint256 creditDeposits) = market.creditDeposits.tryGet(assets[i]);
if (!exists) revert Errors.MarketDoesNotContainTheAsset(assets[i]);
if (creditDeposits == 0) revert Errors.AssetAmountIsZero(assets[i]);
// cache usdc address
address usdc = MarketMakingEngineConfiguration.load().usdc;
// creditDeposits in zaros internal precision so convert to native token decimals
ctx.creditDepositsNativeDecimals =
Collateral.load(assets[i]).convertUd60x18ToTokenAmount(ud60x18(creditDeposits));
// convert the assets to USDC; both input and outputs in native token decimals
@> uint256 usdcOut = _convertAssetsToUsdc(
@> dexSwapStrategyIds[i], assets[i], ctx.creditDepositsNativeDecimals, paths[i], address(this), usdc
@> );
// sanity check to ensure we didn't somehow give away the input tokens
if (usdcOut == 0) revert Errors.ZeroOutputTokens();
// settles the credit deposit for the amount of USDC received
// updating storage so convert from native token decimals to zaros internal precision
market.settleCreditDeposit(assets[i], Collateral.load(usdc).convertTokenAmountToUd60x18(usdcOut));
// emit an event
emit LogConvertMarketCreditDepositsToUsdc(marketId, assets[i], creditDeposits, usdcOut);
}
}

If any of the protocolFeeRecipients is blacklisted, the safeTransfer call will fail, reverting the entire function and blocking rewards for all recipients. USDC and other centralized assets have blacklisting mechanisms (e.g., USDC uses Circle’s blacklist feature). If any fee recipient is blacklisted, the call to safeTransfer fails, preventing further execution of the function.

Impact

If one recipient is blacklisted, none of the recipients receive their rewards because the function reverts entirely.

Tools Used

Manual Review

Recommendations

Modify the function to Handle Blacklisted Recipients Gracefully

Updates

Lead Judging Commences

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

A blacklisted fee recipient will DoS the distributeProtocolAssetReward function because there's no way of removing them from array.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.