Part 2

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

Fee-on-Transfer Token Dust Accumulation in Reward Distribution

Summary

The distributeProtocolAssetReward function in MarketMakingEngineConfiguration.sol does not account for fee-on-transfer tokens, leading to unrecoverable residual funds ("dust") in the protocol contract. This occurs because the function assumes the full amount specified is transferred to recipients, while fee-on-transfer tokens deduct a fee during transfers, reducing the actual balance sent.


Vulnerability Details

Location

MarketMakingEngineConfiguration.soldistributeProtocolAssetReward function
Code Snippet:

function distributeProtocolAssetReward(Data storage self, address asset, uint256 amount) internal {
UD60x18 totalFeeRecipientsSharesX18 = ud60x18(self.totalFeeRecipientsShares);
UD60x18 amountX18 = ud60x18(amount);
uint256 totalDistributed = 0;
for (uint256 i; i < self.protocolFeeRecipients.length(); i++) {
(address feeRecipient, uint256 shares) = self.protocolFeeRecipients.at(i);
uint256 feeRecipientReward = amountX18.mul(ud60x18(shares)).div(totalFeeRecipientsSharesX18).intoUint256();
totalDistributed += feeRecipientReward;
// Adjust last transfer to avoid rounding errors
if (i == self.protocolFeeRecipients.length() - 1) {
feeRecipientReward += amount - totalDistributed;
}
IERC20(asset).safeTransfer(feeRecipient, feeRecipientReward);
}
}

Technical Analysis

  1. Root Cause:

    • Fee-on-Transfer Mechanics: Tokens like USDT (with a transfer fee) deduct a percentage (e.g., 1%) from the sent amount.

    • Incorrect Balance Assumption: The function calculates feeRecipientReward based on the input amount, not the actual token balance after transfers.

  2. Example Scenario:

    • Input: amount = 100 tokens, 2 recipients with 50% shares each, 10% transfer fee.

    • Expected Behavior:

      • Recipient 1 receives 45 tokens (50 - 10% fee).

      • Recipient 2 receives 45 tokens (50 - 10% fee).

      • Total transferred: 90 tokens.

    • Actual Behavior:

      • Recipient 1 receives 45 tokens (protocol balance: 55).

      • Recipient 2 attempts to receive 50 tokens (due to amount - totalDistributed = 50), but only 49.5 are available (55 - 10% fee).

      • Result: Transaction reverts or leaves 5.5 tokens trapped in the contract.


Impact

  • Medium Severity

    • Dust Accumulation: Residual tokens accumulate in the contract, becoming unrecoverable without a dedicated recovery mechanism.

    • Reward Shortfalls: Fee recipients receive less than their entitled share due to unaccounted transfer fees.

    • Transaction Failures: For tokens with high fees or large distributions, later transfers may revert due to insufficient balances.


Recommendations

Immediate Fixes

  1. Track Actual Transferred Amounts:
    Modify the distribution logic to use pre/post transfer balances:

    uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
    IERC20(asset).safeTransfer(feeRecipient, feeRecipientReward);
    uint256 balanceAfter = IERC20(asset).balanceOf(address(this));
    uint256 actualTransferred = balanceBefore - balanceAfter;
    totalDistributed += actualTransferred;
  2. Adjust Last Recipient Logic:
    Replace the flat amount - totalDistributed adjustment with a balance-based calculation:

    if (i == feeRecipientsLength - 1) {
    feeRecipientReward = IERC20(asset).balanceOf(address(this));
    }

Long-Term Mitigations

  1. Token Allowlist:
    Restrict reward assets to non-fee-on-transfer tokens via a curated allowlist.

  2. Recovery Mechanism:
    Add a function to recover stuck tokens (e.g., recoverDust(address asset)).

  3. Documentation Warnings:
    Explicitly state that fee-on-transfer tokens are unsupported.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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