Skip to content

Latest commit

 

History

History
98 lines (66 loc) · 4.19 KB

File metadata and controls

98 lines (66 loc) · 4.19 KB

Stable Cyan Stallion

Medium

Users can bypass tax amount for airdrop claims during the final 26 minutes/1572 seconds/131 blocks of the claiming period

Summary

Due to the calculation in _calculateClaimTaxAmount(), users can claim their airdrop tokens without paying any tax during the final 26 minutes (1572 seconds) of the airdrop claiming period. This vulnerability stems from a precision issue where division precedes multiplication in the tax calculation formula.

Root Cause

In the _calculateClaimTaxAmount() function, taxFee is derived by multiplying $.maxChargeableTax by the remaining claiming time, and then dividing by AIRDROP_CLAIMING_PERIOD_LENGTH

        uint256 taxFee = $.maxChargeableTax.mulDiv(claimingTimeLeft, AIRDROP_CLAIMING_PERIOD_LENGTH);
        claimTaxAmount = claimerUsd0PPBalance.mulDiv(taxFee, BASIS_POINT_BASE);

https://github.com/sherlock-audit/2024-10-usual-labs-v1/blob/main/pegasus/packages/solidity/src/airdrop/AirdropTaxCollector.sol#L232C9-L233C80

When the claimingTimeLeft approaches very low values near the end of the airdrop period, the calculation for claimTaxAmount fails to capture a meaningful tax fee. Specifically:

  • The tax calculation divides $.maxChargeableTax by the claiming period AIRDROP_CLAIMING_PERIOD_LENGTH before multiplying it by claimingTimeLeft.
  • This division results in a zero or near-zero tax fee when claimingTimeLeft becomes small, allowing users to avoid paying tax within a "free claim window" as the period ends.

Example Scenario

  1. Assume AIRDROP_CLAIMING_PERIOD_LENGTH = 182 days and $.maxChargeableTax = BASIS_POINT_BASE = 10,000.
  2. With claimingTimeLeft = 1572 seconds (approximately 26 minutes) before the claiming period ends.
  3. The existing formula of tax fee calculation would result in a zero claimTaxAmount due to precision loss, allowing users to avoid tax entirely if they claim in this final 26-minute period.

AIRDROP_CLAIMING_PERIOD_LENGTH = 182 days = 15,724,800 seconds $.maxChargeableTax = BASIS_POINT_BASE = 10,000 claimerUsd0PPBalance = 1e18 claimingTimeLeft = 1572 seconds taxFee = ( 10,000 * 1572 ) / 15,724,800 = 0 claimTaxAmount = ( 1e18 * 0 ) / 10,000 = 0

Internal pre-conditions

  1. An airdrop has started and the claiming window has reached to last 26 minutes

External pre-conditions

No response

Attack Path

  1. Any claim at this moment can be performed without paying any tax amount.

Impact

This vulnerability creates an unintended "free claim window" towards the end of the airdrop period, where users can avoid the intended tax by claiming just before the period expires. This could lead to significant losses in expected tax revenue for the protocol.

PoC

    function testDivisionBeforeMultiplication()
        external
        userHasPrelaunchBalance(alice, 1500 ether)
        afterClaimingPeriodStart(AIRDROP_CLAIMING_PERIOD_LENGTH - 1572) // 1572 is 26 Minute and 12 Seconds
    {
        uint256 taxAmount = airdropTaxCollector.calculateClaimTaxAmount(alice);
        assertEq(taxAmount, 0);
    }

Please place this test case in packages/solidity/test/airdrop/AirdropTaxCollector.t.sol and run forge test --match-test testDivisionBeforeMultiplication

Mitigation

To resolve this issue, change the tax calculation formula like:

    function _calculateClaimTaxAmount(AirdropTaxCollectorStorage storage $, address account)
        internal
        view
        returns (uint256 claimTaxAmount)
    {
        uint256 claimerUsd0PPBalance = $.prelaunchUsd0ppBalance[account];

        uint256 claimingTimeLeft;
        if (block.timestamp > AIRDROP_INITIAL_START_TIME + AIRDROP_CLAIMING_PERIOD_LENGTH) {
            claimingTimeLeft = 0;
        } else {
            claimingTimeLeft =
                AIRDROP_INITIAL_START_TIME + AIRDROP_CLAIMING_PERIOD_LENGTH - block.timestamp;
        }

-       uint256 taxFee = $.maxChargeableTax.mulDiv(claimingTimeLeft, AIRDROP_CLAIMING_PERIOD_LENGTH);
-       claimTaxAmount = claimerUsd0PPBalance.mulDiv(taxFee, BASIS_POINT_BASE);
+       claimTaxAmount = claimerUsd0PPBalance.mulDiv($.maxChargeableTax * claimingTimeLeft, AIRDROP_CLAIMING_PERIOD_LENGTH * BASIS_POINT_BASE);
    }