QuantAMM

QuantAMM
49,600 OP
View results
Submission Details
Severity: high
Valid

NFT Transfers Reset Uplift Fee Basis Allowing Complete Fee Avoidance

Vulnerability Details

The UpliftOnlyExample contract implements a fee mechanism where users pay uplift fees based on the value appreciation of their liquidity position. The contract tracks positions using NFTs, where each NFT represents a deposit and its associated fee data in the poolsFeeData mapping. When users withdraw, they pay fees based on the value increase from their deposit price.

The contract includes an afterUpdate() function that handles NFT transfers between users. However, this function contains an issue in how it updates fee data during transfers:

UpliftOnlyExample.sol#L606-L614

if (_to != address(0)) {
feeDataArray[tokenIdIndex].lpTokenDepositValue = lpTokenDepositValueNow;
feeDataArray[tokenIdIndex].blockTimestampDeposit = uint32(block.number);
feeDataArray[tokenIdIndex].upliftFeeBps = upliftFeeBps;
poolsFeeData[poolAddress][_to].push(feeDataArray[tokenIdIndex]);
}

The issue is that during transfers, the contract completely resets the fee basis by updating the deposit value to the current value, effectively erasing any accumulated uplift. This allows users to bypass uplift fees entirely by transferring their NFT before withdrawal, as the new owner's position will be based on the transfer-time price rather than the original deposit price.

Impact

High economic issue that allows complete bypass of the protocol's uplift fee mechanism through NFT transfers, resulting in up to 96% reduction in fee collection and undermining the entire economic model of the protocol.

Proof of Concept

Alice can exploit this vulnerability through the following steps:

  1. Alice deposits 1000 tokens when price is $100

    • lpTokenDepositValue set to $100,000

    • Stored in poolsFeeData[pool][alice]

  2. Price increases to $150 (+50%)

    • Alice would normally pay uplift fee on 50% gain

    • With upliftFeeBps = 500, fee would be 25 tokens

  3. Instead of withdrawing, Alice transfers NFT to Bob

    // In afterUpdate()
    feeDataArray[tokenIdIndex].lpTokenDepositValue = lpTokenDepositValueNow; // Sets to $150
    poolsFeeData[poolAddress][bob].push(feeDataArray[tokenIdIndex]);
  4. Bob receives position with:

    • New deposit value of $150

    • No accumulated uplift

    • Can withdraw immediately paying only minWithdrawalFeeBps (0.1%)

    • Actual fee: 1 token instead of 25 tokens

  5. Optional: Bob transfers back to Alice who can now withdraw with minimum fee

Tools Used

Manual Review

Recommendation

Implement one or both of these solutions:

  1. Preserve original deposit data during transfers:

function afterUpdate(address _from, address _to, uint256 _tokenID) public {
// ... existing checks ...
if (_to != address(0)) {
// Don't update deposit values, just copy existing data
poolsFeeData[poolAddress][_to].push(feeDataArray[tokenIdIndex]);
// Clean up old data
if (tokenIdIndex != feeDataArrayLength - 1) {
for (uint i = tokenIdIndex + 1; i < feeDataArrayLength; i++) {
feeDataArray[i - 1] = feeDataArray[i];
}
}
delete feeDataArray[feeDataArrayLength - 1];
feeDataArray.pop();
}
}
  1. Add a transfer fee based on accumulated uplift:

function afterUpdate(address _from, address _to, uint256 _tokenID) public {
// ... existing checks ...
if (_to != address(0)) {
uint256 uplift = (lpTokenDepositValueNow - feeDataArray[tokenIdIndex].lpTokenDepositValue) /
feeDataArray[tokenIdIndex].lpTokenDepositValue;
uint256 transferFee = uplift * feeDataArray[tokenIdIndex].upliftFeeBps;
// Collect transfer fee before allowing transfer
}
}
Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_afterUpdate_bypass_fee_collection_updating_the_deposited_value

Likelihood: High, any transfer will trigger the bug. Impact: High, will update lpTokenDepositValue to the new current value without taking fees on profit.

Support

FAQs

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