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;) {
locals.res = int256(_prevWeights[locals.i])
+ locals.kappa[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
@>
@> newWeightsConverted[locals.i] = locals.res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
for (locals.i = 0; locals.i < _prevWeights.length;) {
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);
@>
@> newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
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);
@>
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);
@>
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) {
locals.normalizationFactor /= int256(locals.prevWeightLength);
for (locals.i = 0; locals.i < locals.prevWeightLength;) {
int256 res = int256(_prevWeights[locals.i])
+ locals.kappaStore[0].mul(locals.newWeights[locals.i] - locals.normalizationFactor);
@>
@> 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);
@>
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);
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]);
@>
@> newWeightsConverted[locals.i] = res;
unchecked {
++locals.i;
}
}
} else {
.
.
...
for (locals.i = 0; locals.i < _prevWeights.length;) {
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
-
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.
-
Economic Instability
-
Operational Failures
-
Smart Contract Errors:
-
Rebalancing Logic:
-
Protocol Integrity Issues
-
Trust and Reputation:
-
Regulatory Concerns:
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;
}