The ChannelFollowingUpdateRule contract allows negative weights to be created in pools under extreme conditions. The weights can go negative within just 5 updates while maintaining total weight at 1e18, breaking fundamental AMM invariants.
The issue occurs in the weight calculation where aggressive parameters and extreme price movements can drive weights negative:
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "@prb/math/contracts/PRBMathSD59x18.sol";
import "../../../contracts/mock/mockRules/MockChannelFollowing.sol";
import "../../../contracts/mock/MockPool.sol";
import "../utils.t.sol";
contract QuantammChannelFollowingNegativeWeightsTest is Test, QuantAMMTestUtils {
MockChannelFollowingRule public rule;
MockPool public mockPool;
function setUp() public {
rule = new MockChannelFollowingRule(address(this));
mockPool = new MockPool(3600, 1 ether, address(rule));
}
function testChannelFollowingNegativeWeightsExtreme() public {
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = 10e18;
parameters[1] = new int256[]();
parameters[1][0] = 0.1e18;
parameters[2] = new int256[]();
parameters[2][0] = 5e18;
parameters[3] = new int256[]();
parameters[3][0] = 0.5e18;
parameters[4] = new int256[]();
parameters[4][0] = 0.541519e18;
parameters[5] = new int256[]();
parameters[5][0] = 0.1e18;
parameters[6] = new int256[]();
parameters[6][0] = 1e18;
int256[] memory prevWeights = new int256[]();
prevWeights[0] = 0.99e18;
prevWeights[1] = 0.01e18;
int256[] memory data = new int256[]();
data[0] = 20e18;
data[1] = 0.05e18;
int256[] memory variances = new int256[]();
variances[0] = 0.001e18;
variances[1] = 1000e18;
int256[] memory prevMovingAverages = new int256[]();
prevMovingAverages[0] = 0.1e18;
prevMovingAverages[1] = 10e18;
prevMovingAverages[2] = 0.1e18;
prevMovingAverages[3] = 0.05e18;
int128[] memory lambda = new int128[]();
lambda[0] = int128(0.999e18);
mockPool.setNumberOfAssets(2);
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
prevMovingAverages,
variances,
2
);
int256[] memory results;
bool foundNegative = false;
for(uint i = 0; i < 10; i++) {
rule.CalculateUnguardedWeights(
prevWeights,
data,
address(mockPool),
parameters,
lambda,
prevMovingAverages
);
results = rule.GetResultWeights();
emit log_named_uint("Update", i + 1);
emit log_named_int("Weight 0", results[0]);
emit log_named_int("Weight 1", results[1]);
emit log_named_int("Total Weight", results[0] + results[1]);
if (results[0] < 0 || results[1] < 0) {
foundNegative = true;
if (results[0] < 0) {
emit log_string("Weight 0 went negative");
}
if (results[1] < 0) {
emit log_string("Weight 1 went negative");
}
break;
}
prevWeights = results;
}
assertEq(
results[0] + results[1],
1e18,
"Total weight should remain exactly 1e18"
);
assertTrue(
foundNegative,
"Should be able to produce negative weights under extreme conditions"
);
}
function abs(int256 x) internal pure returns (int256) {
return x >= 0 ? x : -x;
}
}
Update: 1
Weight 0: 902884388529738820
Weight 1: 97115611470261190
Total Weight: 1000000000000000010
Update: 2
Weight 0: 719073687365397700
Weight 1: 280926312634602320
Total Weight: 1000000000000000020
Update: 3
Weight 0: 438669156159542750
Weight 1: 561330843840457280
Total Weight: 1000000000000000030
Update: 4
Weight 0: 61773881363725370
Weight 1: 938226118636274670
Total Weight: 1000000000000000040
Update: 5
Weight 0: -411507299345927140
Weight 1: 1411507299345927190
Total Weight: 1000000000000000050
Weight 0 went negative