QuantAMM

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

Users can mint NFTs without adding liquidity to the protocol

Summary

When users add liquidity to QuantAMM by calling the UpliftOnlyExample::addLiquidityProportional function, they are minted an NFT in addition to the BPTs they receive.
However, because the UpliftOnlyExample::addLiquidityProportional function does not check to ensure at least one non-zero input amount, users can keep minting the NFT without adding liquidity to QuantAMM.

Vulnerability Details

The vulnerability lies in the fact that the UpliftOnlyExample::addLiquidityProportional function does not revert whenever a user populates the maxAmountsIn array with zeros signifying that no liquidity is added to the protocol. The affected code can be viewed on github here and also provided below for each of reference

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

Impact

Users can keep minting NFTs without adding liquidity to the protocol. Meanwhile, the NFTs are meant for liquidity providers. Since NFTs can be highly valued, it implies that such users can get rich off the protocol without making any significant contribution to the protocol.

Note that a user can mint up to 100 NFTs in each liquidity pool in QuantAMM without providing liquidity in any pool.

Tools Used

Manual Review

Foundry

Proof of Concept:

  1. A malicious user bob sees that they can mint NFTs without providing liquidity to QuantAMM

  2. bob populates the maxAmountsIn parameter array with zeros and calls the UpliftOnlyExample::addLiquidityProportional

  3. bob mints himself an NFT without adding liquidity to the protocol

  4. In fact, bob can do this 100 times for each liquidity pool on the QuantAMM

PoC Place the following code into `UpliftExample.t.sol`.
function test_SpomariaPoC_UserCanMintNFTWithoutAddingLiquidity() public {
BaseVaultTest.Balances memory balancesBefore = getBalances(bob);
uint256 zeroBpt = 0;
uint256[] memory maxAmountsIn = [uint256(0), uint256(0)].toMemoryArray();
vm.prank(bob);
/*uint256[] memory amountsInFirst = */upliftOnlyRouter.addLiquidityProportional(
pool,
maxAmountsIn,
zeroBpt,
false,
bytes("")
);
vm.stopPrank();
assertEq(upliftOnlyRouter.getUserPoolFeeData(pool, bob)[0].tokenID, 1, "tokenID incorrect");
assertEq(upliftOnlyRouter.getUserPoolFeeData(pool, bob)[0].amount, 0, "amount incorrect");
assertEq(upliftOnlyRouter.lpNFT().ownerOf(1), bob);
// bob mints nft again without adding liquidity
vm.prank(bob);
/*uint256[] memory amountsInSecond = */upliftOnlyRouter.addLiquidityProportional(
pool,
maxAmountsIn,
zeroBpt,
false,
bytes("")
);
vm.stopPrank();
assertEq(upliftOnlyRouter.getUserPoolFeeData(pool, bob)[1].tokenID, 2, "tokenID incorrect");
assertEq(upliftOnlyRouter.getUserPoolFeeData(pool, bob)[1].amount, 0, "amount incorrect");
assertEq(upliftOnlyRouter.lpNFT().ownerOf(2), bob);
}

Recommendations

Consider adding a check in the UpliftOnlyExample::addLiquidityProportional to proceed only if they is at least one non-zero element in the maxAmountsIn array as shown below:

function addLiquidityProportional(
address pool,
uint256[] memory maxAmountsIn,
uint256 exactBptAmountOut,
bool wethIsEth,
bytes memory userData
) external payable saveSender(msg.sender) returns (uint256[] memory amountsIn) {
+ for (uint256 i; i < maxAmountsIn.length; i++){
+ if(maxAmountsIn[i] > 0){
+ break;
+ } else {
+ if (i == maxAmountsIn.length - 1) revert();
+ }
+ }
if (poolsFeeData[pool][msg.sender].length > 100) {
revert TooManyDeposits(pool, msg.sender);
}
.
.
.
}

Better still, the zero in the condition if(maxAmountsIn[i] > 0) in the recommendation above can be replaced with a minimum amount allowed to add liquidity if the prototocol wants.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas / Admin is trusted / Pool creation is trusted / User mistake / Suppositions

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelyhood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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

Give us feedback!