QuantAMM

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

Weights are clamped wrongly so new weights are calculated wrongly as leading to broken weights.

Summary

Weights are clamped wrongly so new weights are calculated wrongly as leading to broken weights.
https://github.com/Cyfrin/2024-12-quantamm/blob/a775db4273eb36e7b4536c5b60207c9f17541b92/pkg/pool-quantamm/contracts/rules/base/QuantammMathGuard.sol#L36

Vulnerability Details

QuantammMathGuard.sol#_guardQuantAMMWeights() function which guards QuantAMM weights is as follows.

function _guardQuantAMMWeights(
int256[] memory _weights,
int256[] calldata _prevWeights,
int256 _epsilonMax,
int256 _absoluteWeightGuardRail
) internal pure returns (int256[] memory guardedNewWeights) {
@> // first see if the weights go beyond the maximum/minimum weights
@> _weights = _clampWeights(_weights, _absoluteWeightGuardRail);
//then reduce even further if the weight change is beyond the allowed "speed limit" that protects the changes from front running
guardedNewWeights = _normalizeWeightUpdates(_prevWeights, _weights, _epsilonMax);
}

As we can see above, _clampWeights() function clamps weights so that they go beyond the maximum/minimum.
This function is as follows.

/// @dev there are some edge cases where the clamping might result to break the guard rail. This is known and the last interpolation block logic in the update weight runner is an ultimate guard against this.
/// @notice Applies guard rails (min value, max value) to weights and returns the normalized weights
/// @param _weights Raw weights
/// @return Clamped weights
function _clampWeights(
int256[] memory _weights,
int256 _absoluteWeightGuardRail
) internal pure returns (int256[] memory) {
unchecked {
uint weightLength = _weights.length;
if (weightLength == 1) {
return _weights;
}
int256 absoluteMin = _absoluteWeightGuardRail;
@> int256 absoluteMax = ONE -
(PRBMathSD59x18.fromInt(int256(_weights.length - 1)).mul(_absoluteWeightGuardRail));
@> int256 sumRemainerWeight = ONE;
int256 sumOtherWeights;
for (uint i; i < weightLength; ++i) {
if (_weights[i] < absoluteMin) {
@> _weights[i] = absoluteMin;
@> sumRemainerWeight -= absoluteMin;
} else if (_weights[i] > absoluteMax) {
@> _weights[i] = absoluteMax;
@> sumOtherWeights += absoluteMax;
}
}
@> if (sumOtherWeights != 0) {
@> int256 proportionalRemainder = sumRemainerWeight.div(sumOtherWeights);
for (uint i; i < weightLength; ++i) {
@> if (_weights[i] != absoluteMin) {
@> _weights[i] = _weights[i].mul(proportionalRemainder);
}
}
}
}
return _weights;
}

From doc, we can see that this function clamps to [minimum, maximum] and normalizes weights.
But above implementation is wrong.

Impact

This wrong implementation leads to not clamped weights and not normalized weights.
This makes the protocol to be broken.

Tools Used

Mannual review

Recommendations

We have to modify QuantammMathGuard.sol#_clampWeights() function as follows.

/// @dev there are some edge cases where the clamping might result to break the guard rail. This is known and the last interpolation block logic in the update weight runner is an ultimate guard against this.
/// @notice Applies guard rails (min value, max value) to weights and returns the normalized weights
/// @param _weights Raw weights
/// @return Clamped weights
function _clampWeights(
int256[] memory _weights,
int256 _absoluteWeightGuardRail
) internal pure returns (int256[] memory) {
unchecked {
uint weightLength = _weights.length;
if (weightLength == 1) {
return _weights;
}
int256 absoluteMin = _absoluteWeightGuardRail;
int256 absoluteMax = ONE -
(PRBMathSD59x18.fromInt(int256(_weights.length - 1)).mul(_absoluteWeightGuardRail));
int256 aver;
int256 w_min = _weights[0], w_max = _weights[0];
for(uint i; i < weightLength; i++){
aver += _weights[i];
if(_weights[i] < w_min) w_min = _weights[i];
if(_weights[i] > w_max) w_max = _weights[i];
}
if(w_min >= absoluteMin && w_max <= absoluteMax){
return _weights;
}
aver /= weightLength;
int256 d_min = aver - w_min;
int256 d_max = w_max - aver;
int256 new_aver = int256(1e18).div(int256(weightLength)); // 1 / n
int256 n_min = new_aver - absoluteMin;
int256 n_max = absoluteMax - new_aver;
int256 rate;
if(d_min != 0){
rate = n_min.div(d_min);
}else{
rate = type(int256).max;
}
if(d_max != 0){
int256 t_rate = n_max.div(d_max);
if(t_rate < rate){
rate = t_rate;
}
}
for(uint i; i < weightLength; i++){
_weights[i] = (_weights[i] - aver).mul(rate) + new_aver;
}
}
}

Description of recommendation

  • We convert weights to w'[i] = (w[i] - a) * lamda + 1 / n.
    Here, a = sum{w} / n. lamda = min{(1 / n - absoluteMin) / (a - w_min), (absoluteMax - 1 / n) / (w_max - a)}.
    w_min = min{w}. w_max = max{w}.

  • Then, sum{w'} = 1. min{w'} >= absoluteMin and max{w'} <= absoluteMax.

Updates

Lead Judging Commences

n0kto Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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

Give us feedback!