QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: medium
Invalid

FILO Withdrawal Pattern Creates Unfair Fee Structure for Long-term LPs

Summary

The UpliftOnlyExample contract uses a FILO (First In Last Out) pattern for processing LP withdrawals, which unfairly penalizes early depositors and long-term liquidity providers by subjecting them to disproportionately higher uplift fees due to the uplift provided by QuantAMM appreciating over time.

Vulnerability Details

The UpliftOnlyExample contract stores deposit receipts in poolsFeeData mapping's fee data array and processes them in FILO order during withdrawals;
The liquidity deposits are done in addLiquidityProportional and they are pushed into the array in sequence.

function addLiquidityProportional(...) {
// ...
uint256 depositValue =
getPoolLPTokenValue(IUpdateWeightRunner(_updateWeightRunner).getData(pool), pool, MULDIRECTION.MULDOWN);
@> poolsFeeData[pool][msg.sender].push(
FeeData({
tokenID: tokenID,
amount: exactBptAmountOut,
//this rounding favours the LP
@> lpTokenDepositValue: depositValue,
//known use of timestamp, caveats are known.
blockTimestampDeposit: uint40(block.timestamp),
upliftFeeBps: upliftFeeBps
})
);

At the time of withdrawal of the liquidity FILO pattern is used as seen below. The loop iterates starting from the end of the array.

function onAfterRemoveLiquidity(...) {
// ...
@> for (uint256 i = localData.feeDataArrayLength - 1; i >= 0; --i) {
localData.lpTokenDepositValue = feeDataArray[i].lpTokenDepositValue;
@> localData.lpTokenDepositValueChange = (
int256(localData.lpTokenDepositValueNow) - int256(localData.lpTokenDepositValue)
) / int256(localData.lpTokenDepositValue);
if (localData.lpTokenDepositValueChange > 0) {
feePerLP = (
uint256(localData.lpTokenDepositValueChange) *
(uint256(feeDataArray[i].upliftFeeBps) * 1e18)
) / 10000;
}
// ...
}
}

Now from the same above function, the feePerLP is calculated for each fee data in the array. now the value of feePerLP is directly proportional to the localData.lpTokenDepositValueChange. Now if we zoom in on the localData.lpTokenDepositValueChange we see that in its calculation it uses difference between the current lp token deposit value and the value recorded when adding liquidity.

This pattern is detrimental to the initial LPs since their very first deposits will keep staying in the poolsFeeData inevitably unless they make a large withdraw that also removes that initial liquidity. I will demonstrate this using the PoC below.

PoC

Now, let's say Alice is very enthusiastic about what QuantAMM has to offer and has onboarded as one of the initial LPs in a pool with Ethereum and USDC using the Momentum Update Rule for simplicity. In the PoC below we assume a continuous bullish momentum for Ethereum price. Also note that I am referring to the entries in the fee data array in the poolsFeeData mapping as deposit receipts.

  1. Alice proportionally adds Liquidity of ETH and the corresponding USDC when ETH price is $2,000 (Jan 2025) and the

    • amount (bpt) recorded: 500

    • LP Token Deposit value recorded: 2,000

  2. Alice makes another deposit (adds Liquidity) in August 2025 when ETH price is $4,000. Note that price appreciation will have caused pool to have more ETH in weight.

    • amount recorded: 400

    • LP Token Deposit value recorded: 4,000

  3. Alice makes another deposit (adds Liquidity) in February 2026 when ETH price is $7,000. Note that price appreciation will have caused pool to have more ETH in weight.

    • amount recorded: 600

    • LP Token Deposit value recorded: 7,000

  4. Alice's total liquidity is now 500 + 400 + 600 = 1,500 BPT. Then Alice withdraws liquidity of 700 BPT in March 2026 when the ether price is still $7,000. Due to FILO processing the 2 latest deposit receipts will be cleared leaving a partial amount in the 2nd deposit and the very 1st deposit.

  5. Assuming in December, 2026, Ethereum price reaches $10,000. Now Alice wishes to withdraw all her liquidity from the pool, now due to such price appreciation the value of lpTokenDepositValueNow when withdrawing liquidity will have to process that partial amount in the 2nd receipt that remained as well as the 1st deposit receipt which has the lowest LP Token Deposit value recorded.

    As a result since the lpTokenDepositValueChange will be very large the fees will be massive and in the span of many years and greater price appreciations the fees will just lead to loss to the LP.
    let's say lpTokenDepositValueNow is 10,000 but at the time of initial deposit it was only 2,000. which is a 400% increase. Keep in mind that this 400% is the lpTokenDepositValueChange which is used to calculate the uplift fee charged.
    now just imagine if we get values like 400%, 1000%, 10,000% what about 1 million percent?.

Conclusively, the current protocol design (FILO) leaves initial deposit receipts dangling which will be a nightmare for the LPs when left in a long time, from the PoC above this issue could have been resolved by priotizing the initial deposit receipts first.

Impact

Early depositors and long-term LPs face disproportionately higher uplift fees when they withdraw their tokens.
Also creates perverse incentive to frequently withdraw and re-deposit to reset deposit values.
Thus it discourages long-term liquidity provision

Tools Used

Manual review

Recommendations

  • Try using the FIFO (First In First Out) pattern:

function onAfterRemoveLiquidity(...) {
// Process from start of array instead of end
for (uint256 i = 0; i < localData.feeDataArrayLength; i++) {
// Process withdrawals
}
}
  • Think of another alternative way to keep track of deposit receipts for the LPs.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

tinnohofficial Submitter
10 months ago
tinnohofficial Submitter
10 months ago
n0kto Lead Judge
10 months ago
n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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

Give us feedback!