QuantAMM

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

Unbalanced Liquidity Caused by Donations in `QuantAMMWeightedPoolFactory.sol` with `disableUnbalancedLiquidity` Flag Enabled

Summary

A critical vulnerability exists where donating tokens to the pool causes unbalanced liquidity even when the disableUnbalancedLiquidity flag is set to true in the QuantAMMWeightedPoolFactory.sol contract. This bypasses an important protocol safeguard meant to maintain balanced pool ratios.

Affected segment of code: https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/QuantAMMWeightedPoolFactory.sol#L131-L173

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/vault/contracts/Router.sol#L212-L233

https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/vault/contracts/Vault.sol#L640C9-L645C54

Vulnerability Details

When the disableUnbalancedLiquidity flag is enabled, the pool is supposed to prevent any operations that would result in unbalanced token ratios. However, the donate() function can be used to circumvent this protection. The provided proof of concept demonstrates that an attacker can donate tokens in arbitrary ratios, disrupting the pool's balanced state.

The vulnerability manifests in the following sequence:

  1. Pool is deployed with disableUnbalancedLiquidity = true

  2. Initial liquidity is added proportionally

  3. The donate function can then be called with unbalanced token amounts

  4. The pool accepts the donation despite the protection flag being set

The test case shows that when donating only DAI (with 0 USDC), the pool's balance ratios become skewed, contradicting the intended behavior of disableUnbalancedLiquidity.

POC

function testCreatePoolWithProportionalLiquidityThenDonate() public {
uint256 amountToDonate = poolInitAmount;
// Step 1: Deploy and initialize the pool
address quantAMMWeightedPool = _deployAndInitializeQuantAMMWeightedPool(true, true);
// Step 2: Add liquidity to the pool using addLiquidityProportional
uint256[] memory maxAmountsIn = new uint256[]();
maxAmountsIn[0] = amountToDonate;
maxAmountsIn[1] = amountToDonate;
uint256 exactBptAmountOut = 1000;
bool wethIsEth = false;
bytes memory userData = "";
// Add liquidity proportionally to the pool
vm.prank(bob);
router.addLiquidityProportional(
quantAMMWeightedPool,
maxAmountsIn,
exactBptAmountOut,
wethIsEth,
userData
);
// Log initial balances before donation attempt
console2.log("=== Initial Balances ===");
console2.log("Pool DAI Balance:", dai.balanceOf(quantAMMWeightedPool));
console2.log("Pool USDC Balance:", usdc.balanceOf(quantAMMWeightedPool));
console2.log("Vault DAI Balance:", dai.balanceOf(address(vault)));
console2.log("Vault USDC Balance:", usdc.balanceOf(address(vault)));
uint256[] memory poolBalancesBefore = vault.getRawBalances(quantAMMWeightedPool);
console2.log("Pool Raw DAI Balance:", poolBalancesBefore[daiIdx]);
console2.log("Pool Raw USDC Balance:", poolBalancesBefore[usdcIdx]);
// Step 3: Try donating an unbalanced amount
vm.prank(bob);
router.donate(
quantAMMWeightedPool,
[amountToDonate, 0].toMemoryArray(), // Donate only DAI
false,
bytes("")
);
// Log balances after donation attempt
console2.log("\n=== After Donation Attempt ===");
console2.log("Pool DAI Balance:", dai.balanceOf(quantAMMWeightedPool));
console2.log("Pool USDC Balance:", usdc.balanceOf(quantAMMWeightedPool));
console2.log("Vault DAI Balance:", dai.balanceOf(address(vault)));
console2.log("Vault USDC Balance:", usdc.balanceOf(address(vault)));
uint256[] memory poolBalancesAfter = vault.getRawBalances(quantAMMWeightedPool);
console2.log("Pool Raw DAI Balance:", poolBalancesAfter[daiIdx]);
console2.log("Pool Raw USDC Balance:", poolBalancesAfter[usdcIdx]);
// Log changes in balances
console2.log("\n=== Balance Changes ===");
console2.log("Pool Raw DAI Change:", int256(poolBalancesAfter[daiIdx]) - int256(poolBalancesBefore[daiIdx]));
console2.log("Pool Raw USDC Change:", int256(poolBalancesAfter[usdcIdx]) - int256(poolBalancesBefore[usdcIdx]));
}

Impact

This vulnerability has several severe implications:

  1. Price manipulation potential through intentionally skewing pool ratios

  2. Arbitrage opportunities that could drain value from legitimate liquidity providers

  3. Undermining of the pool's core protection mechanism

  4. Potential for economic attacks through strategic unbalancing

The severity is high because it bypasses a critical protocol safety mechanism and could lead to direct financial losses for users.

Tools Used

  • Foundry testing framework

  • Manual code review

Recommendations

  1. Modify the donate() function in the router to enforce balance checks when disableUnbalancedLiquidity is true:

function donate(
bytes32 poolId,
uint256[] memory amounts,
bool fromInternalBalance,
bytes memory userData
) external override {
if (disableUnbalancedLiquidity) {
// Add proportionality check
_validateProportionalDonation(amounts);
}
// Existing donation logic
}
  1. Implement a proportionality validation function:

function _validateProportionalDonation(uint256[] memory amounts) internal view {
uint256[] memory balances = getPoolBalances();
for (uint256 i = 1; i < amounts.length; i++) {
require(
amounts[i] * balances[0] == amounts[0] * balances[i],
"Non-proportional donation"
);
}
}
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.

Appeal created

0xslowbug Submitter
10 months ago
n0kto Lead Judge
10 months ago
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!