The QuantAMMWeightedPool implementation enforces an undocumented minimum swap amount of 1001 wei, despite the mathematical model and whitepaper suggesting that any non-zero amount should be valid. This creates unexpected behavior for integrators and could affect protocols attempting to execute minimal test trades or precise arbitrage.
The issue manifests when attempting to execute swaps with amounts less than 1001 wei. Despite the mathematical model supporting any non-zero amount, the implementation returns 0 for output amounts when the input is below this threshold.
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { PoolRoleAccounts, SwapKind, PoolSwapParams } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol";
import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol";
import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol";
import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol";
import { QuantAMMWeightedPool } from "../../../contracts/QuantAMMWeightedPool.sol";
import { QuantAMMWeightedPoolFactory } from "../../../contracts/QuantAMMWeightedPoolFactory.sol";
import { QuantAMMWeightedPoolContractsDeployer } from "../utils/QuantAMMWeightedPoolContractsDeployer.sol";
import { OracleWrapper } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/OracleWrapper.sol";
import { MockUpdateWeightRunner } from "../../../contracts/mock/MockUpdateWeightRunner.sol";
import { MockMomentumRule } from "../../../contracts/mock/mockRules/MockMomentumRule.sol";
import { MockChainlinkOracle } from "../../../contracts/mock/MockChainlinkOracles.sol";
import { IUpdateRule } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IUpdateRule.sol";
import { IQuantAMMWeightedPool } from "@balancer-labs/v3-interfaces/contracts/pool-quantamm/IQuantAMMWeightedPool.sol";
contract QuantammWeightedPoolTest is QuantAMMWeightedPoolContractsDeployer, BaseVaultTest {
using CastingHelpers for address[];
using ArrayHelpers for *;
using FixedPoint for uint256;
uint64 public constant MAX_SWAP_FEE_PERCENTAGE = 10e16;
QuantAMMWeightedPoolFactory internal quantAMMWeightedPoolFactory;
function setUp() public override {
int216 fixedValue = 1000;
uint delay = 3600;
super.setUp();
(address ownerLocal, address addr1Local, address addr2Local) = (vm.addr(1), vm.addr(2), vm.addr(3));
owner = ownerLocal;
addr1 = addr1Local;
addr2 = addr2Local;
vm.startPrank(owner);
updateWeightRunner = new MockUpdateWeightRunner(owner, addr2, false);
chainlinkOracle = _deployOracle(fixedValue, delay);
updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle));
vm.stopPrank();
quantAMMWeightedPoolFactory = deployQuantAMMWeightedPoolFactory(
IVault(address(vault)),
365 days,
"Factory v1",
"Pool v1"
);
vm.label(address(quantAMMWeightedPoolFactory), "quantamm weighted pool factory");
}
function _createPoolParams() internal returns (QuantAMMWeightedPoolFactory.NewPoolParams memory retParams) {
PoolRoleAccounts memory roleAccounts;
IERC20[] memory tokens = [
address(dai),
address(usdc),
address(weth),
address(wsteth),
address(veBAL),
address(waDAI),
address(usdt),
address(waUSDC)
].toMemoryArray().asIERC20();
MockMomentumRule momentumRule = new MockMomentumRule(owner);
int256[] memory initialWeights = new int256[]();
uint256[] memory initialWeightsUint = new uint256[]();
for(uint i = 0; i < 8; i++) {
initialWeights[i] = 0.125e18;
initialWeightsUint[i] = 0.125e18;
}
uint64[] memory lambdas = new uint64[]();
lambdas[0] = 0.2e18;
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = 0.2e18;
address[][] memory oracles = new address[][]();
oracles[0] = new address[]();
oracles[0][0] = address(chainlinkOracle);
retParams = QuantAMMWeightedPoolFactory.NewPoolParams(
"Pool With Donation",
"PwD",
vault.buildTokenConfig(tokens),
initialWeightsUint,
roleAccounts,
MAX_SWAP_FEE_PERCENTAGE,
address(0),
true,
false,
ZERO_BYTES32,
initialWeights,
IQuantAMMWeightedPool.PoolSettings(
new IERC20[](8),
IUpdateRule(momentumRule),
oracles,
60,
lambdas,
0.01e18,
0.01e18,
0.01e18,
parameters,
address(0)
),
initialWeights,
initialWeights,
3600,
0,
new string[][]()
);
}
function _getDefaultBalances() internal pure returns (uint256[] memory balances) {
balances = new uint256[](8);
balances[0] = 1000e18;
balances[1] = 2000e18;
balances[2] = 500e18;
balances[3] = 350e18;
balances[4] = 750e18;
balances[5] = 7500e18;
balances[6] = 8000e18;
balances[7] = 5000e18;
}
function testFindMinimumViaBinarySearch() public {
QuantAMMWeightedPoolFactory.NewPoolParams memory params = _createPoolParams();
(address pool, ) = quantAMMWeightedPoolFactory.create(params);
uint256[] memory balances = _getDefaultBalances();
uint256 low = 1;
uint256 high = 1e18;
uint256 lastWorking = 0;
for(uint i = 0; i < 60; i++) {
uint256 mid = (low + high) / 2;
PoolSwapParams memory swapParams = PoolSwapParams({
kind: SwapKind.EXACT_IN,
amountGivenScaled18: mid,
balancesScaled18: balances,
indexIn: 0,
indexOut: 7,
router: address(router),
userData: abi.encode(0)
});
vm.prank(address(vault));
uint256 amountOut = QuantAMMWeightedPool(pool).onSwap(swapParams);
if(amountOut > 0) {
high = mid;
lastWorking = mid;
} else {
low = mid + 1;
}
}
console.log("Minimum working amount:", lastWorking);
}
}