QuantAMM

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

Silent Value Normalization in QuantAMMStorage Affecting Weights and Prices

Summary

The QuantAMM storage contract silently normalizes stored values using undocumented thresholds. Values between 1e9 and 2e9 are normalized to 1e9, and values below 1e9 are silently zeroed. This affects all values stored using this mechanism, including both pool weights and prices, potentially leading to unexpected pool behavior when oracle-provided values fall within these ranges.

Vulnerability Details

Location: pkg/pool-quantamm/contracts/QuantAMMStorage.sol

The issue occurs in the storage encoding/decoding process where values are scaled by 1e9 during packing/unpacking operations. The critical code paths are:

  1. Packing (storing values):

function quantAMMPackEight32(
int256 _firstInt,
// ... other params ...
) internal pure returns (int256 packed) {
require(
_firstInt <= MAX32 &&
_firstInt >= MIN32 &&
// ... other bounds checks ...
"Overflow"
);
int256 firstPacked = int256(uint256(_firstInt << 224) >> 224) << 224;
// ... packing other values ...
packed = firstPacked | // ... other packed values
}
  1. Unpacking (retrieving values):

function quantAMMUnpack32(int256 sourceElem) internal pure returns (int256[] memory targetArray) {
targetArray = new int256[](8);
targetArray[0] = (sourceElem >> 224) * 1e9;
targetArray[1] = int256(int32(sourceElem >> 192)) * 1e9;
targetArray[2] = int256(int32(sourceElem >> 160)) * 1e9;
// ... unpacking other values ...
return targetArray;
}

The combination of:

  • int32 bounds (±2.147e9)

  • 1e9 scaling factor

  • Bit shifting operations

  • Silent truncation

Creates several issues:

  1. Values that don't cleanly divide by 1e9 lose precision

  2. Values outside int32 bounds after scaling are truncated

  3. The truncation happens silently without warning or events

Test results demonstrate the behavior:

Index 2
Input: 1999999999
Output: 1000000000 // Value truncated during packing/unpacking
Index 6
Input: 500000000
Output: 0 // Value lost due to scaling

This behavior affects all operations using the storage contract, including pool weights and price calculations.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../../../contracts/mock/MockQuantAMMStorage.sol";
import { QuantAMMTestUtils } from "../utils.t.sol";
contract QuantAMMStorageEdgeCasesTest is Test, QuantAMMTestUtils {
MockQuantAMMStorage internal mockQuantAMMStorage;
function setUp() public {
mockQuantAMMStorage = new MockQuantAMMStorage();
}
// Test 1: Bit Pattern Edge Cases
function testBitPatternEdgeCases() public {
int256[] memory values = new int256[]();
// Test critical bit patterns
values[0] = 2000000000; // 2e9 instead of type(int32).max
values[1] = -2000000000; // -2e9 instead of type(int32).min
values[2] = 1999999999; // Just under 2e9
values[3] = -1999999999; // Just under -2e9
values[4] = 1000000000; // 1e9
values[5] = -1000000000; // -1e9
values[6] = 500000000; // 5e8
values[7] = -500000000; // -5e8
int256[] memory result = mockQuantAMMStorage.ExternalEncodeDecode32Array(values, values.length);
// Print values for debugging
for(uint i = 0; i < values.length; i++) {
console.log("Index", i);
console.log("Input:", uint(values[i]));
console.log("Output:", uint(result[i]));
}
for(uint i = 0; i < values.length; i++) {
assertEq(values[i], result[i], "Bit pattern corruption");
}
}
}

Test Results:

Index 0
Input: 2000000000
Output: 2000000000
Index 1
Input: 115792089237316195423570985008687907853269984665640564039457584007911129639936
Output: 115792089237316195423570985008687907853269984665640564039457584007911129639936
Index 2
Input: 1999999999
Output: 1000000000
Index 3
Input: 115792089237316195423570985008687907853269984665640564039457584007911129639937
Output: 115792089237316195423570985008687907853269984665640564039457584007912129639936
Index 4
Input: 1000000000
Output: 1000000000
Index 5
Input: 115792089237316195423570985008687907853269984665640564039457584007912129639936
Output: 115792089237316195423570985008687907853269984665640564039457584007912129639936
Index 6
Input: 500000000
Output: 0
Index 7
Input: 115792089237316195423570985008687907853269984665640564039457584007912629639936
Output: 0

Impact

Severity: HIGH

Justification:

  1. Impact: HIGH

    • Complete zeroing of values below 1e9

    • Silent normalization of values between 1e9 and 2e9 to 1e9

    • Affects both weights and prices in the pool

    • No events or warnings when values are modified

    • Could lead to significant pool imbalances

    • Breaks core AMM invariants

  2. Likelihood: MEDIUM

    • Occurs whenever oracle values fall in affected ranges

    • No validation or bounds checking on oracle inputs

    • Multiple values affected (both weights and prices)

    • Silent failure mode makes detection difficult

    • No recovery mechanism once values are corrupted

  3. Technical Impact:

    • Silent normalization of values between 1e9 and 2e9 to 1e9

    • Complete zeroing of values below 1e9

    • No events emitted for value modifications

    • No reversion on unexpected values

    • Affects all stored values including weights and prices

  4. Economic Impact:

    • Incorrect price calculations due to normalized/zeroed values

    • Pool imbalances from incorrect weight ratios

    • Loss of precision in both weight and price calculations

    • Potential for significant economic loss if oracle values consistently fall in affected ranges

    • While direct manipulation isn't possible (oracle-controlled), the impact when it occurs is severe

Note: While oracle manipulation is out of scope, the silent failure mode of the storage contract when handling valid oracle values represents a significant risk to the protocol's core functionality.

Tools Used

  • Foundry testing framework

  • Manual code review

  • Custom test suite for storage behavior

  • Console logging for value verification

Recommendations

  1. Add Explicit Value Validation:

function validateWeight(int256 weight) internal pure {
require(weight >= 0, "Weight must be positive");
require(weight <= 2e9, "Weight exceeds maximum");
require(weight >= 1e9 || weight == 0, "Weight below minimum");
}
  1. Implementation Changes:

    • Add explicit validation before packing/unpacking operations

    • Emit events for any value modifications

    • Document the valid ranges and behavior in comments

    • Consider using a different scaling factor to avoid precision loss

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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