QuantAMM

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

Extreme Weight Amplification in ChannelFollowingUpdateRule with Small Price Changes

Summary

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.

Vulnerability Details

Location: pkg/pool-quantamm/contracts/rules/ChannelFollowingUpdateRule.sol

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.

Proof of Concept

// SPDX-License-Identifier: MIT
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; // Varying kappa
params.parameters[1] = new int256[]();
params.parameters[1][0] = 0.001e18; // Small width
params.parameters[2] = new int256[]();
params.parameters[2][0] = 100e18; // High amplitude
params.parameters[3] = new int256[]();
params.parameters[3][0] = 3e18; // Moderate exponent
params.parameters[4] = new int256[]();
params.parameters[4][0] = 0.001e18; // Small inverse scaling
params.parameters[5] = new int256[]();
params.parameters[5][0] = 0.001e18; // Small pre-exp scaling
params.parameters[6] = new int256[]();
params.parameters[6][0] = PRBMathSD59x18.fromInt(1); // Use raw price
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) {
// Reset pool and rule state for this calculation
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();
// Create new parameters for second calculation to avoid modifying the original
TestParameters memory params2 = setupTestParameters(params.parameters[0][0]);
params2.data[1] = PRBMathSD59x18.fromInt(100) + 0.000001e18;
// Reset pool and rule for second calculation
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));
// Test with series of minimal changes
int256[] memory priceDeltas = new int256[]();
priceDeltas[0] = 1; // 1 wei
priceDeltas[1] = 10; // 10 wei
priceDeltas[2] = 100; // 100 wei
priceDeltas[3] = 1000; // 1000 wei
priceDeltas[4] = 10000; // 10000 wei
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("");
// Check if weights still sum to 1 (within tight tolerance)
require(
abs(weightDiff0 + weightDiff1) <= 1, // Allow at most 1 wei imbalance
"Weight changes do not balance within 1 wei"
);
}
}
}

Test Results:

Price delta (wei): 0.000000000000000001
Weight change 0: 5149.053569817570338824
Weight change 1: -5149.053569817570338825
Weight balance error: -0.000000000000000001
Price delta (wei): 0.000000000000000010
Weight change 0: 5149.053569817570338824
Weight change 1: -5149.053569817570338825
Weight balance error: -0.000000000000000001

Problematic Parameter Values:

params.parameters[1][0] = 0.001e18; // Small width
params.parameters[2][0] = 100e18; // High amplitude
params.parameters[4][0] = 0.001e18; // Small inverse scaling

Attack Scenario

  1. Attacker identifies pool with sensitive parameters

  2. Makes minimal price changes (1 wei)

  3. Triggers large weight updates (~5149 change)

  4. Exploits resulting price movements through arbitrage

  5. Repeats process for continued profit

Impact

Severity: HIGH

  1. Technical Impact:

    • Disproportionate weight changes from minimal price movements

    • Consistent -1 wei imbalance in weight changes

    • Parameter combinations create ~100,000,000x amplification

    • Violates whitepaper's "smooth interpolation" principle

    • Affects all pools using ChannelFollowingUpdateRule

  2. Economic Impact:

    • Excessive slippage for normal traders

    • Arbitrage opportunities from weight imbalances

    • Potential for pool manipulation

    • Higher gas costs from frequent rebalancing

    • Reduced pool efficiency

Tools Used

  • Foundry testing framework

  • Custom test suite for weight change analysis

  • Mathematical analysis of amplification factors

  • Parameter sensitivity testing

Recommendations

  1. Implement Parameter Bounds:

function validateParameters(
int256 width,
int256 amplitude,
int256 inverseScaling
) internal pure {
require(width >= 0.1e18, "Width too small");
require(amplitude <= 10e18, "Amplitude too large");
require(inverseScaling >= 0.1e18, "Inverse scaling too small");
// Check combined amplification factor
int256 maxAmplification = amplitude
.mul(ONE.div(width))
.mul(ONE.div(inverseScaling));
require(maxAmplification <= 1000e18, "Amplification factor too high");
}
  1. Add Gradual Weight Adjustment:

    • Implement maximum per-block weight change

    • Use time-weighted average for price changes

    • Add smoothing function for weight updates

    • Consider implementing weight change delays

  2. Improve Parameter Selection:

    • Width: ≥ 0.1e18 (larger values reduce sensitivity)

    • Amplitude: ≤ 10e18 (smaller values reduce impact)

    • Inverse scaling: ≥ 0.1e18 (larger values reduce amplification)

    • Consider automated parameter optimization

References

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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