The ChannelFollowingUpdateRule contract exhibits extreme amplification of weight changes when processing small price movements. A 1 wei price change can result in weight changes of ~5149, which could lead to excessive slippage, arbitrage opportunities, and potential pool manipulation.
The issue occurs due to parameter combinations that create massive amplification of tiny price movements. The channel following formula's components (width, amplitude, and inverse scaling) can combine to create multiplication factors in the thousands.
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../../../contracts/mock/MockRuleInvoker.sol";
import "../../../contracts/mock/mockRules/MockChannelFollowing.sol";
import "../../../contracts/mock/MockPool.sol";
import "../utils.t.sol";
contract ChannelFollowingUpdateRuleAuditTest is Test, QuantAMMTestUtils {
MockChannelFollowingRule rule;
MockPool mockPool;
int256 private previousWeightDiff;
struct TestParameters {
int256[][] parameters;
int256[] prevAlphas;
int256[] prevMovingAverages;
int256[] movingAverages;
int128[] lambdas;
int256[] prevWeights;
int256[] data;
}
function setUp() public {
rule = new MockChannelFollowingRule(address(this));
mockPool = new MockPool(3600, PRBMathSD59x18.fromInt(1), address(rule));
}
function setupTestParameters(int256 kappa) private pure returns (TestParameters memory) {
TestParameters memory params;
params.parameters = new int256[][]();
params.parameters[0] = new int256[]();
params.parameters[0][0] = kappa;
params.parameters[1] = new int256[]();
params.parameters[1][0] = 0.001e18;
params.parameters[2] = new int256[]();
params.parameters[2][0] = 100e18;
params.parameters[3] = new int256[]();
params.parameters[3][0] = 3e18;
params.parameters[4] = new int256[]();
params.parameters[4][0] = 0.001e18;
params.parameters[5] = new int256[]();
params.parameters[5][0] = 0.001e18;
params.parameters[6] = new int256[]();
params.parameters[6][0] = PRBMathSD59x18.fromInt(1);
params.prevAlphas = new int256[](2);
params.prevAlphas[0] = PRBMathSD59x18.fromInt(1);
params.prevAlphas[1] = PRBMathSD59x18.fromInt(100);
params.prevMovingAverages = new int256[](2);
params.prevMovingAverages[0] = PRBMathSD59x18.fromInt(1);
params.prevMovingAverages[1] = PRBMathSD59x18.fromInt(100);
params.movingAverages = new int256[](2);
params.movingAverages[0] = PRBMathSD59x18.fromInt(1);
params.movingAverages[1] = PRBMathSD59x18.fromInt(100);
params.lambdas = new int128[](1);
params.lambdas[0] = int128(0.999e18);
params.prevWeights = new int256[](2);
params.prevWeights[0] = 0.5e18;
params.prevWeights[1] = 0.5e18;
params.data = new int256[](2);
params.data[0] = PRBMathSD59x18.fromInt(1);
params.data[1] = PRBMathSD59x18.fromInt(100);
return params;
}
function calculateWeightChange(TestParameters memory params) private returns (int256, int256) {
mockPool = new MockPool(3600, PRBMathSD59x18.fromInt(1), address(rule));
mockPool.setNumberOfAssets(2);
rule = new MockChannelFollowingRule(address(this));
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
params.prevMovingAverages,
params.prevAlphas,
mockPool.numAssets()
);
rule.CalculateUnguardedWeights(
params.prevWeights,
params.data,
address(mockPool),
params.parameters,
params.lambdas,
params.movingAverages
);
int256[] memory resultWeights1 = rule.GetResultWeights();
TestParameters memory params2 = setupTestParameters(params.parameters[0][0]);
params2.data[1] = PRBMathSD59x18.fromInt(100) + 0.000001e18;
mockPool = new MockPool(3600, PRBMathSD59x18.fromInt(1), address(rule));
mockPool.setNumberOfAssets(2);
rule = new MockChannelFollowingRule(address(this));
rule.initialisePoolRuleIntermediateValues(
address(mockPool),
params2.prevMovingAverages,
params2.prevAlphas,
mockPool.numAssets()
);
rule.CalculateUnguardedWeights(
params2.prevWeights,
params2.data,
address(mockPool),
params2.parameters,
params2.lambdas,
params2.movingAverages
);
int256[] memory resultWeights2 = rule.GetResultWeights();
return (
resultWeights2[0] - resultWeights1[0],
resultWeights2[1] - resultWeights1[1]
);
}
function testWeightBalanceWithMinimalChanges() public {
TestParameters memory params = setupTestParameters(PRBMathSD59x18.fromInt(1));
int256[] memory priceDeltas = new int256[]();
priceDeltas[0] = 1;
priceDeltas[1] = 10;
priceDeltas[2] = 100;
priceDeltas[3] = 1000;
priceDeltas[4] = 10000;
for (uint i = 0; i < priceDeltas.length; i++) {
params.data[0] = 1e18;
params.data[1] = 1e18 + priceDeltas[i];
(int256 weightDiff0, int256 weightDiff1) = calculateWeightChange(params);
emit log_named_decimal_int("Price delta (wei)", priceDeltas[i], 18);
emit log_named_decimal_int("Weight change 0", weightDiff0, 18);
emit log_named_decimal_int("Weight change 1", weightDiff1, 18);
emit log_named_decimal_int("Weight balance error", weightDiff0 + weightDiff1, 18);
emit log("");
require(
abs(weightDiff0 + weightDiff1) <= 1,
"Weight changes do not balance within 1 wei"
);
}
}
}