QuantAMM

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

New weight calculated to be negative is not restricted. Breaking TFMM Pricing model and LPs could lose revenue.

Summary

In the PowerChannelUpdateRule, MomentumUpdateRule MinimumVarianceUpdateRule, DifferenceMomentumUpdateRule, ChannelFollowingUpdateRule, and AntiMomentumUpdateRule contracts, they have an internal override function called _getWeights which calculates weights and forward calculated new weights to its caller. Due to an omission, a negative weight result can be stored into newWeightsConverted.

Vulnerability Details

PowerChannelUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (locals.kappa.length == 1) {
locals.normalizationFactor /= int256(locals.prevWeightsLength);
for (locals.i = 0; locals.i < locals.prevWeightsLength;) {
//κ · ( sign(1/p(t)*∂p(t)/∂t) * |1/p(t)*∂p(t)/∂t|^q − ℓp(t)
locals.res = int256(_prevWeights[locals.i])
+ locals.kappa[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
@> // @info: missing check if res is greater than or equal to 0
@> newWeightsConverted[locals.i] = locals.res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
for (locals.i = 0; locals.i < _prevWeights.length;) {
//κ · ( sign(1/p(t)*∂p(t)/∂t) * |1/p(t)*∂p(t)/∂t|^q − ℓp(t)
locals.res = int256(_prevWeights[locals.i])
+ locals.kappa[locals.i].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
@> require(locals.res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = locals.res;
unchecked {
++locals.i;
}
}
}
return newWeightsConverted;
}

MomentumUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (locals.kappaStore.length == 1) {
.
.
...
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
int256 res = int256(_prevWeights[locals.i])
+ locals.kappaStore[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
@> // @info: missing check if res is greater than or equal to 0
@> newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
// To avoid intermediate overflows (because of normalization), we only downcast in the end to an uint6
for (locals.i = 0; locals.i < _prevWeights.length;) {
locals.res = int256(_prevWeights[locals.i])
+ locals.kappaStore[locals.i].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
@> require(locals.res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = locals.res;
unchecked {
++locals.i;
}
}
}
return newWeightsConverted;
}

MinimumVarianceUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters, //
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (_parameters[0].length == 1) {
.
.
...
for (uint256 i; i < _prevWeights.length;) {
int256 res = mixingVariance.mul(int256(_prevWeights[i])) + newWeights[i].div(divisionFactor);
@> // @info: missing check if res is greater than or equal to 0
newWeightsConverted[i] = res;
unchecked {
++i;
}
}
} else {
.
.
...
for (uint256 i; i < _prevWeights.length;) {
int256 mixingVariance = _parameters[0][i];
int256 res = mixingVariance.mul(int256(_prevWeights[i])) + newWeights[i].div(divisionFactor);
@> // @info: missing check if res is greater than or equal to 0
newWeightsConverted[i] = res;
unchecked {
++i;
}
}
}
return newWeightsConverted;
}

DifferenceMomentumUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
@> // @info: dEaDcOdE
@> for (uint256 i; i < newShortMovingAverages.length;) {
unchecked {
++i;
}
}
.
.
...
if (locals.kappaStore.length == 1) {
//scalar logic separate to vector for efficiency
locals.normalizationFactor /= int256(locals.prevWeightLength);
// To avoid intermediate overflows (because of normalization), we only downcast in the end to an uint6
// κ · ( (EWMA_short - EWMA_long) / EWMA_long − ℓp(t))
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
int256 res = int256(_prevWeights[locals.i])
+ locals.kappaStore[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
@> // @info: missing check if res is greater than or equal to 0
@> newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
}
return newWeightsConverted;
}

ChannelFollowingUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (locals.kappa.length == 1) {
locals.normalizationFactor /= int256(locals.prevWeightLength);
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
newWeightsConverted[locals.i] =
_prevWeights[locals.i] + locals.kappa[0].mul(locals.signal[locals.i] - locals.normalizationFactor);
@> // @info: missing check if newWeightsConverted[locals.i] is greater than or equal to 0
unchecked {
++locals.i;
}
}
} else {
.
.
...
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
int256 weightUpdate = locals.kappa[locals.i].mul(locals.signal[locals.i] - locals.normalizationFactor);
newWeightsConverted[locals.i] = _prevWeights[locals.i] + weightUpdate;
@> require(newWeightsConverted[locals.i] >= 0, "Invalid weight");
unchecked {
++locals.i;
}
}
}
return newWeightsConverted;
}

AntiMomentumUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (locals.kappa.length == 1) {
locals.normalizationFactor /= int256(_prevWeights.length);
// w(t − 1) + κ ·(ℓp(t) − 1/p(t) · ∂p(t)/∂t)
for (locals.i = 0; locals.i < _prevWeights.length;) {
int256 res = int256(_prevWeights[locals.i])
+ int256(locals.kappa[0]).mul(locals.normalizationFactor - locals.newWeights[locals.i]);
@> // @info: missing check if res is greater than or equal to 0
@> newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
for (locals.i = 0; locals.i < _prevWeights.length;) {
// w(t − 1) + κ ·(ℓp(t) − 1/p(t) · ∂p(t)/∂t)
int256 res = int256(_prevWeights[locals.i])
+ int256(locals.kappa[locals.i]).mul(locals.normalizationFactor - locals.newWeights[locals.i]);
@> require(res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
}
return newWeightsConverted;
}

As we can see that in functions, We're missing that require check that is necessary to avoid negative result values.

Impact

  1. Mathematical Breakdowns

    • Pricing Formula Failures: In TFMM, G3M, or any AMM model, weights are used to balance liquidity and determine swap prices. A negative weight would:

      • Cause calculations to yield nonsensical results.

      • Lead to division by zero or undefined behavior in logarithmic or exponential functions.

      • Corrupt price determination and make swaps impossible or irrational.

    • Covariance-Based Rebalancing: If weights are used in covariance calculations, a negative value could invert the intended relationship between assets, leading to erratic behavior during rebalancing.

  2. Economic Instability

    • Mispricing and Arbitrage:

      • A negative weight could result in massive mispricing of assets.

      • Arbitrageurs might exploit the imbalance to drain liquidity, leading to losses for the liquidity pool and its participants.

    • LP Losses:

      • Liquidity providers (LPs) rely on weights to ensure proportional allocation of their deposits. A negative weight could distort allocations, rendering their positions valueless or even liabilities.

  3. Operational Failures

    • Smart Contract Errors:

      • Many operations assume weights are non-negative. A negative weight might lead to:

        • Overflow or underflow in arithmetic operations.

        • Reversion of transactions due to failed assertions or invalid state transitions.

    • Rebalancing Logic:

      • Rebalancing functions expect weights to sum to a positive constant (like 1 or 1e18). A negative weight could break this invariant, halting the pool’s operations.

  4. Protocol Integrity Issues

    • Trust and Reputation:

      • Allowing negative weights could signal poor governance or safeguards, causing LPs and traders to lose confidence in the protocol.

    • Regulatory Concerns:

      • If a negative weight inadvertently leads to LP losses, the protocol could face legal or reputational consequences.

Tools Used

Manual Review

Recommendations

Solution is simple, Please add that require check to restrict negative result values. We can do something have done below...

PowerChannelUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (locals.kappa.length == 1) {
locals.normalizationFactor /= int256(locals.prevWeightsLength);
for (locals.i = 0; locals.i < locals.prevWeightsLength;) {
//κ · ( sign(1/p(t)*∂p(t)/∂t) * |1/p(t)*∂p(t)/∂t|^q − ℓp(t)
locals.res = int256(_prevWeights[locals.i])
+ locals.kappa[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
+ require(locals.res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = locals.res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
}
return newWeightsConverted;
}

MomentumUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (locals.kappaStore.length == 1) {
//scalar logic separate to vector for efficiency
locals.normalizationFactor /= int256(locals.prevWeightLength);
// To avoid intermediate overflows (because of normalization), we only downcast in the end to an uint6
// κ · ( 1/p(t) * ∂p(t)/∂t − ℓp(t))
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
int256 res = int256(_prevWeights[locals.i])
+ locals.kappaStore[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
+ require(locals.res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
}
return newWeightsConverted;
}

MinimumVarianceUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters, //
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (_parameters[0].length == 1) {
.
.
...
for (uint256 i; i < _prevWeights.length;) {
int256 res = mixingVariance.mul(int256(_prevWeights[i])) + newWeights[i].div(divisionFactor);
+ require(res >= 0, "Invalid weight");
newWeightsConverted[i] = res;
unchecked {
++i;
}
}
} else {
.
.
...
for (uint256 i; i < _prevWeights.length;) {
int256 mixingVariance = _parameters[0][i];
int256 res = mixingVariance.mul(int256(_prevWeights[i])) + newWeights[i].div(divisionFactor);
+ require(res >= 0, "Invalid weight");
newWeightsConverted[i] = res;
unchecked {
++i;
}
}
}
return newWeightsConverted;
}

DifferenceMomentumUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
- for (uint256 i; i < newShortMovingAverages.length;) {
- unchecked {
- ++i;
- }
- }
.
.
...
if (locals.kappaStore.length == 1) {
//scalar logic separate to vector for efficiency
locals.normalizationFactor /= int256(locals.prevWeightLength);
// To avoid intermediate overflows (because of normalization), we only downcast in the end to an uint6
// κ · ( (EWMA_short - EWMA_long) / EWMA_long − ℓp(t))
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
int256 res = int256(_prevWeights[locals.i])
+ locals.kappaStore[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
+ require(res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
}
return newWeightsConverted;
}

ChannelFollowingUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (locals.kappa.length == 1) {
locals.normalizationFactor /= int256(locals.prevWeightLength);
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
- newWeightsConverted[locals.i] =
- _prevWeights[locals.i] + locals.kappa[0].mul(locals.signal[locals.i] - locals.normalizationFactor);
+ int256 res =
+ _prevWeights[locals.i] + locals.kappa[0].mul(locals.signal[locals.i] - locals.normalizationFactor);
+ require(res >= 0, "Invalid weight");
+ newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
int256 weightUpdate = locals.kappa[locals.i].mul(locals.signal[locals.i] - locals.normalizationFactor);
newWeightsConverted[locals.i] = _prevWeights[locals.i] + weightUpdate;
require(newWeightsConverted[locals.i] >= 0, "Invalid weight");
unchecked {
++locals.i;
}
}
}
return newWeightsConverted;
}

AntiMomentumUpdateRule::_getWeights:

function _getWeights(
int256[] calldata _prevWeights,
int256[] memory _data,
int256[][] calldata _parameters,
QuantAMMPoolParameters memory _poolParameters
) internal override returns (int256[] memory newWeightsConverted) {
.
.
...
if (locals.kappa.length == 1) {
locals.normalizationFactor /= int256(_prevWeights.length);
// w(t − 1) + κ ·(ℓp(t) − 1/p(t) · ∂p(t)/∂t)
for (locals.i = 0; locals.i < _prevWeights.length;) {
int256 res = int256(_prevWeights[locals.i])
+ int256(locals.kappa[0]).mul(locals.normalizationFactor - locals.newWeights[locals.i]);
+ require(res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
for (locals.i = 0; locals.i < _prevWeights.length;) {
// w(t − 1) + κ ·(ℓp(t) − 1/p(t) · ∂p(t)/∂t)
int256 res = int256(_prevWeights[locals.i])
+ int256(locals.kappa[locals.i]).mul(locals.normalizationFactor - locals.newWeights[locals.i]);
require(res >= 0, "Invalid weight");
newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
}
return newWeightsConverted;
}
Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

invalid_weights_can_be_negative_or_extreme_values

_clampWeights will check that these weights are positive and in the boundaries before writing them in storage.

Support

FAQs

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

Give us feedback!