QuantAMM

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

The block number is used instead of the block timestamp for updating or transferring liquidity and LPNFT. This oversight introduces challenges in accurately tracking intervals for adding and removing liquidity and LPNFT.

Summary

In the UpliftOnlyExample contract, block.timestamp is used to track liquidity additions and is recorded in the FeeData struct's state variable poolsFeeData, a mapping keyed by the pool address and owner address. While the potential caveats of using block.timestamp are well-understood and handled with care, an oversight exists in the UpliftOnlyExample::afterUpdate function. This function is responsible for tracking and updating data when transferring LPNFT. Instead of block.timestamp, block.number is used for the FeeData member feeDataArray[tokenIdIndex].blockTimestampDeposit. This design choice complicates the tracking of data, particularly in monitoring and updating intervals for liquidity deposits and transfers in FeeData.

Vulnerability Details

Refer to the code snippet below. The block.timestamp is utilized in the addLiquidityProportional function to track liquidity deposits.

UpliftOnlyExample::addLiquidityProportional:

function addLiquidityProportional(
address pool,
uint256[] memory maxAmountsIn,
uint256 exactBptAmountOut,
bool wethIsEth,
bytes memory userData
) external payable saveSender(msg.sender) returns (uint256[] memory amountsIn) {
if (poolsFeeData[pool][msg.sender].length > 100) {
revert TooManyDeposits(pool, msg.sender);
}
// Do addLiquidity operation - BPT is minted to this contract.
amountsIn = _addLiquidityProportional(
pool, msg.sender, address(this), maxAmountsIn, exactBptAmountOut, wethIsEth, userData
);
uint256 tokenID = lpNFT.mint(msg.sender);
//this requires the pool to be registered with the QuantAMM update weight runner
//as well as approved with oracles that provide the prices
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
})
);
nftPool[tokenID] = pool;
}

As mentioned above, the comment indicates that all the caveats associated with using timestamps are well-understood.

Now, let’s take a closer look at updating or transferring liquidity (LPNFT)...

UpliftOnlyExample::afterUpdate:

function afterUpdate(address _from, address _to, uint256 _tokenID) public {
if (msg.sender != address(lpNFT)) {
revert TransferUpdateNonNft(_from, _to, msg.sender, _tokenID);
}
address poolAddress = nftPool[_tokenID];
if (poolAddress == address(0)) {
revert TransferUpdateTokenIDInvaid(_from, _to, _tokenID);
}
int256[] memory prices = IUpdateWeightRunner(_updateWeightRunner).getData(poolAddress);
uint256 lpTokenDepositValueNow = getPoolLPTokenValue(prices, poolAddress, MULDIRECTION.MULDOWN);
FeeData[] storage feeDataArray = poolsFeeData[poolAddress][_from];
uint256 feeDataArrayLength = feeDataArray.length;
uint256 tokenIdIndex;
bool tokenIdIndexFound = false;
//find the tokenID index in the array
for (uint256 i; i < feeDataArrayLength; ++i) {
if (feeDataArray[i].tokenID == _tokenID) {
tokenIdIndex = i;
tokenIdIndexFound = true;
break;
}
}
if (tokenIdIndexFound) {
if (_to != address(0)) {
// Update the deposit value to the current value of the pool in base currency (e.g. USD) and the block index to the current block number
//vault.transferLPTokens(_from, _to, feeDataArray[i].amount);
feeDataArray[tokenIdIndex].lpTokenDepositValue = lpTokenDepositValueNow;
@> // @info: using block.number instead of block.timestamp
// on adding a new deposit, the block.timestamp is used
// on transfer, the block.number is used
// could emerge issues with the block.timestamp and block.number difference
// on removing liquidity and transferring the NFT
// therefore users could face a dos attack
// and we're kinda adding a new deposit on transfer
// so it's better to use block.timestamp here
@> feeDataArray[tokenIdIndex].blockTimestampDeposit = uint32(block.number);
feeDataArray[tokenIdIndex].upliftFeeBps = upliftFeeBps;
//actual transfer not a afterTokenTransfer caused by a burn
poolsFeeData[poolAddress][_to].push(feeDataArray[tokenIdIndex]);
if (tokenIdIndex != feeDataArrayLength - 1) {
//Reordering the entire array could be expensive but it is the only way to keep true FILO
for (uint256 i = tokenIdIndex + 1; i < feeDataArrayLength; i++) {
delete feeDataArray[i - 1];
feeDataArray[i - 1] = feeDataArray[i];
}
}
delete feeDataArray[feeDataArrayLength - 1];
feeDataArray.pop();
}
}
}

read the comments above indicated by @>.

PoC

UpliftOnlyExample::addLiquidityProportional:

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
})
);

UpliftOnlyExample::afterUpdate:

@> // @info: using block.number instead of block.timestamp
// on adding a new deposit, the block.timestamp is used
// on transfer, the block.number is used
// could emerge issues with the block.timestamp and block.number difference
// on removing liquidity and transferring the NFT
// therefore users could face a dos attack
// and we're kinda adding a new deposit on transfer
// so it's better to use block.timestamp here
@> feeDataArray[tokenIdIndex].blockTimestampDeposit = uint32(block.number);

Impact

  1. Loss of Tracking: Unable to effectively track LPNFT transfers.

  2. Lack of Interval Clarity: No clear record of liquidity addition time intervals.

  3. Unpredictable User Actions: Inability to determine when users transferred their LPNFTs.

  4. Unrecorded Activity: User actions may go undocumented.

  5. Strategic Challenges: Hinders the development of effective strategies.

  6. Non-Linear Progression: Block numbers do not correspond linearly to time, complicating time estimation.

  7. Difficulty Bomb Impact: Sudden jumps in block numbers due to the upcoming difficulty bomb.

  8. Hard Fork Disruptions: Major network upgrades could reset block numbers.

  9. Network Splits: Block numbers may become discontinuous across chains during network splits.

Tools Used

  1. Manual Review

  2. Phind (AI Buddy)

Recommendations

The mitigation for this issue is straightforward. Since the protocol is already aware of the prominent caveats associated with block.timestamp, we should use block.timestamp instead of block.number. The corrected code is provided below.

function afterUpdate(address _from, address _to, uint256 _tokenID) public {
if (msg.sender != address(lpNFT)) {
revert TransferUpdateNonNft(_from, _to, msg.sender, _tokenID);
}
address poolAddress = nftPool[_tokenID];
if (poolAddress == address(0)) {
revert TransferUpdateTokenIDInvaid(_from, _to, _tokenID);
}
int256[] memory prices = IUpdateWeightRunner(_updateWeightRunner).getData(poolAddress);
uint256 lpTokenDepositValueNow = getPoolLPTokenValue(prices, poolAddress, MULDIRECTION.MULDOWN);
FeeData[] storage feeDataArray = poolsFeeData[poolAddress][_from];
uint256 feeDataArrayLength = feeDataArray.length;
uint256 tokenIdIndex;
bool tokenIdIndexFound = false;
//find the tokenID index in the array
for (uint256 i; i < feeDataArrayLength; ++i) {
if (feeDataArray[i].tokenID == _tokenID) {
tokenIdIndex = i;
tokenIdIndexFound = true;
break;
}
}
if (tokenIdIndexFound) {
if (_to != address(0)) {
// Update the deposit value to the current value of the pool in base currency (e.g. USD) and the block index to the current block number
//vault.transferLPTokens(_from, _to, feeDataArray[i].amount);
feeDataArray[tokenIdIndex].lpTokenDepositValue = lpTokenDepositValueNow;
- feeDataArray[tokenIdIndex].blockTimestampDeposit = uint32(block.number);
+ feeDataArray[tokenIdIndex].blockTimestampDeposit = uint40(block.timestamp);
feeDataArray[tokenIdIndex].upliftFeeBps = upliftFeeBps;
//actual transfer not a afterTokenTransfer caused by a burn
poolsFeeData[poolAddress][_to].push(feeDataArray[tokenIdIndex]);
if (tokenIdIndex != feeDataArrayLength - 1) {
//Reordering the entire array could be expensive but it is the only way to keep true FILO
for (uint256 i = tokenIdIndex + 1; i < feeDataArrayLength; i++) {
delete feeDataArray[i - 1];
feeDataArray[i - 1] = feeDataArray[i];
}
}
delete feeDataArray[feeDataArrayLength - 1];
feeDataArray.pop();
}
}
}
Updates

Lead Judging Commences

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

finding_afterUpdate_blockNumber_instead_of_timestamp

Likelihood: Medium/High, any NFT transfer will change this variable. Impact: Informational/Very Low. This variable is unused and won’t impact anything, but the array is public and its getter will return a variable with inconsistencies.

Support

FAQs

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

Give us feedback!