The QuantAMMMathGuard contract is an abstract contract that implements the guard rails for QuantAMM weights and is inherited by the UpdateRule contract. The problem is that there is an issue in the implementation that leads to incorrect calculation of weights, and even zero or negative weights.
The contract has two main functions: _clampWeights() and _normalizeWeightUpdates(). The first function checks for minimum and maximum values of the weights, and if any of them fall outside the specified range, it corrects them. The second function normalizes the newly obtained weights by limiting excessively fast-growing ones and ensures that the sum of the weights remains 1.
https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/rules/base/QuantammMathGuard.sol#L45-L67
Let’s examine the _clampWeights() function. The minimum weight value used is passed as an argument—_absoluteWeightGuardRail. The maximum value is calculated by subtracting n - 1 times the minimum value from 1, where n is the number of assets. Then, all the weights are iterated over, and if any weights are smaller than the minimum value, they are set to the minimum. If there are any weights larger than the maximum, they are set to the maximum. Two auxiliary variables, sumRemainerWeight and sumOtherWeights, are used to track how the values change if there are adjustments. At the end, a second iteration occurs, and some of the weights are normalized to preserve the sum of 1. The problem is that there is a flaw in the implementation, and the last part may not execute - if sumOtherWeights = 0, meaning if there were no weights exceeding the maximum in the initial set.
Let’s look at an example with specific weights. For example, four assets with weights (0.3, 0.2, 0.4, 0.1). Their sum is 1. Let’s set _absoluteWeightGuardRail = 0.2. Then absoluteMax = 1 - (4 - 1) * 0.2 = 0.4. So, the minimum is 0.2 and the maximum is 0.4. It can be seen that the weight list contains a weight lower than the minimum- 0.1. There are no weights greater than absoluteMax. Therefore, this weight is changed from 0.1 to 0.2, and the new list of weights becomes (0.3, 0.2, 0.4, 0.2). The sum of the new weights is 1.1 > 1. Since sumOtherWeights is 0, the final part of the function does not execute, and a weight list is returned with a sum greater than 1 by 10%.
https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/rules/base/QuantammMathGuard.sol#L109
Now, let’s examine the _normalizeWeightUpdates() function. Without going into unnecessary details, in short, the function looks for the maximum absolute change, and if it exceeds the pre-set limit epsilonMax, the weights are adjusted accordingly. If epsilonMax is not exceeded, the weights are simply summed. At the end, if the sum is different from 1, the difference is added to or subtracted from the first element. In the comments, it is stated that the value added or subtracted would be negligibly small, but in the example, we will see that this is not the case and it can result in a zero or negative value for the first weight or a value that is not proportional to the others, which could violate the protocol's risk management and lead to a loss of funds.
Let’s examine _normalizeWeightUpdates() in the context of the example above, with epsilonMax = 0.2 (for simplicity). Suppose the previous weights were (0.5, 0.1, 0.3, 0.1), with the goal being not to exceed epsilonMax and therefore not to change the weights. At the end of the function, (1 - 1.1) will be subtracted from the first weight, reducing it by 0.1. In this case, this is 33% of the weight (0.3), which will lead to an incorrect change in the reserves, inconsistent with the strategy being used, and consequently to a loss of funds. However, the first weight could have been 0.1 (if the positions of the first and last weights were swapped). In that case, the weight becomes 0. In another scenario, if the total sum is smaller than 1 or the difference is larger (greater than 1), a negative value could result. This would break the protocol in many ways, and it is important to note that zero or negative weights are not allowed.
Loss of funds and malfunctioning of the protocol.
Manual review
It is difficult to provide a solution, but the simplest one is that when the sum is different from 1, the difference should be distributed proportionally across all the weights, instead of being added only to the first weight.
According the sponsor and my understanding, sum of weights does not have to be exactly 1 to work fine. So no real impact here. Please provide a PoC showing a realistic impact if you disagree. This PoC cannot contains negative weights because they will be guarded per clampWeights.
Likelihood: Medium/High, when a weight is above absoluteMax. Impact: Low/Medium, weights deviate much faster, and sum of weights also.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.