QuantAMM

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

Lack of Breakglass Manual Override in `UpdateWeightRunner` Contract

Summary

The UpdateWeightRunner contract lacks sufficient safeguards to ensure at least one breakglass mask is set (MASK_POOL_OWNER_UPDATES or MASK_POOL_QUANTAMM_ADMIN_UPDATES). This vulnerability leads to a scenario where pools are entirely locked out from manual overrides if no breakglass permissions are configured during pool initialization. As a result, no authorized party (manager or admin) can intervene in case of emergencies, such as oracle failures or rule malfunctions.

Vulnerability Details

Root Cause

The vulnerability stems from the lack of validation for a "breakglass" mask in the setWeightsManually function. If neither MASK_POOL_OWNER_UPDATES (8) nor MASK_POOL_QUANTAMM_ADMIN_UPDATES (16) is set in the poolRegistry, all manual override attempts fail without an option for recovery, leaving the pool stuck.

The following check is missing in pools without breakglass configurations:

require(
(poolRegistryEntry & (MASK_POOL_OWNER_UPDATES | MASK_POOL_QUANTAMM_ADMIN_UPDATES)) != 0,
"No breakglass mask set!"
);

This results in no authorized party being able to manually intervene during emergencies, such as oracle failures or rule misconfigurations. Without these bits, the pool is permanently locked, relying entirely on automated updates which may not function correctly.

Affected Code

  • Function setWeightsManually:
    Lacks a validation step to ensure that at least one breakglass bit is active before manual weight setting. This omission allows pools to be deployed or used without any emergency override options.
    UpdateWeightRunner.sol - setWeightsManually

Impact

  1. Manual Override Blockage: Pools with poolRegistry = 0 become permanently locked, as no authorized party can perform manual overrides in emergencies.

  2. Functional Stagnation: If automated rules fail or oracles return stale data, the pool remains inoperable without any intervention path.

  3. Funds Risk: Locked pools prevent users from rebalancing weights or handling liquidity issues, leading to potential financial losses.

Proof of Concept

Test

  • Add the following test to pkg/pool-quantamm/test/foundry/UpdateWeightRunner.t.sol:

function testLackOfBreakglassManualOverrideVulnerability() public {
// Step 1: Setup addresses (owner, manager, and random user)
console2.log('===== STEP 1: Setup addresses =====');
address owner = vm.addr(111);
address manager = vm.addr(222);
address randomUser = vm.addr(333);
// Step 2: Deploy the UpdateWeightRunner with a dummy ETH oracle address
console2.log('===== STEP 2: Deploying MockUpdateWeightRunner =====');
vm.startPrank(owner);
MockUpdateWeightRunner runner = new MockUpdateWeightRunner(
owner,
address(0xDEAD),
false
);
console2.log('Runner deployed at:', address(runner));
vm.stopPrank();
// Step 3: Deploy a mock pool referencing the runner (using an arbitrary updateInterval of 3600)
console2.log('===== STEP 3: Deploying MockQuantAMMBasePool =====');
MockQuantAMMBasePool pool = new MockQuantAMMBasePool(3600, address(runner));
console2.log('Pool deployed at:', address(pool));
// No breakglass perms: registry set to 0 (critical part of the vulnerability)
console2.log('===== Setting poolRegistry to 0 => No breakglass perms =====');
pool.setPoolRegistry(0);
// Step 4: Prepare sample weights
console2.log(
'===== STEP 4: Trying to manually set weights from different roles ====='
);
int256[] memory weights = new int256[]();
weights[0] = 0.2e18; // 0.2
weights[1] = 0.3e18; // 0.3
// Attempt manual override from the OWNER
console2.log('Try override from OWNER (should fail)...');
vm.startPrank(owner);
vm.expectRevert('No permission to set weight values');
runner.setWeightsManually(weights, address(pool), 100, 2);
vm.stopPrank();
// Attempt manual override from the MANAGER
console2.log('Try override from MANAGER (should fail)...');
vm.startPrank(manager);
vm.expectRevert('No permission to set weight values');
runner.setWeightsManually(weights, address(pool), 100, 2);
vm.stopPrank();
// Attempt manual override from a RANDOM USER
console2.log('Try override from RANDOM USER (should fail)...');
vm.startPrank(randomUser);
vm.expectRevert('No permission to set weight values');
runner.setWeightsManually(weights, address(pool), 100, 2);
vm.stopPrank();
console2.log(
'All manual override attempts reverted => Lack of breakglass vulnerability confirmed.'
);
}

Test Result

Logs:
===== STEP 1: Setup addresses =====
===== STEP 2: Deploying MockUpdateWeightRunner =====
Runner deployed at: 0x7768251F619f0D2684B103B43E467adD06709cEB
===== STEP 3: Deploying MockQuantAMMBasePool =====
Pool deployed at: 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
===== Setting poolRegistry to 0 => No breakglass perms =====
===== STEP 4: Trying to manually set weights from different roles =====
Try override from OWNER (should fail)...
Try override from MANAGER (should fail)...
Try override from RANDOM USER (should fail)...
All manual override attempts reverted => Lack of breakglass vulnerability confirmed.
  • Result: The test confirms that when poolRegistry = 0 (no breakglass mask set), all attempts to call setWeightsManually(...) fail, regardless of whether the caller is the owner, manager, or a random user. This proves the vulnerability: without a breakglass mask, no manual intervention is possible, leaving the pool permanently locked in emergencies.

Tools Used

  • Foundry: to write and run the test suite.

  • Manual Code Review: Performed a detailed inspection of the contract's logic to identify the lack of validation for breakglass masks.

Recommendations

  1. Enforce Breakglass Masks: Add a require statement in the setWeightsManually(...) function to ensure at least one breakglass mask is active:

    require(
    (poolRegistryEntry & (MASK_POOL_OWNER_UPDATES | MASK_POOL_QUANTAMM_ADMIN_UPDATES)) != 0,
    "No breakglass mask set!"
    );
  2. Default Mask Assignment: Automatically assign at least one mask (MASK_POOL_OWNER_UPDATES or MASK_POOL_QUANTAMM_ADMIN_UPDATES) when initializing pools. Emit warnings or revert during initialization if no mask is set.

  3. Audit Existing Deployments: Review all pools currently deployed to verify that at least one breakglass mask is active. Update or redeploy pools where this is missing.

Validation Through Testing

  • After adding the recommended mitigation to the UpdateWeightRunner contract, include the following test in pkg/pool-quantamm/test/foundry/UpdateWeightRunner.t.sol:

function testBreakglassMaskNotSetReverts() public {
console2.log(
"===== TEST: No breakglass => revert('No breakglass mask set!') ====="
);
// STEP 1: Deploy the UpdateWeightRunner (mock or real)
address owner = vm.addr(111);
address admin = vm.addr(222);
vm.startPrank(owner);
MockUpdateWeightRunner runner = new MockUpdateWeightRunner(
owner,
admin,
false
);
vm.stopPrank();
// STEP 2: Deploy a MockQuantAMMBasePool which references the runner
MockQuantAMMBasePool pool = new MockQuantAMMBasePool(3600, address(runner));
// STEP 3: Set poolRegistry to 0 (no MASK_POOL_OWNER_UPDATES or MASK_POOL_QUANTAMM_ADMIN_UPDATES)
pool.setPoolRegistry(0);
// STEP 4: Prepare minimal weights
int256[] memory weights = new int256[]();
weights[0] = 0.1e18;
weights[1] = 0.2e18;
// STEP 5: Attempt to call setWeightsManually => expect revert with "No breakglass mask set!"
vm.startPrank(owner);
vm.expectRevert('No breakglass mask set!');
runner.setWeightsManually(weights, address(pool), 100, 2);
vm.stopPrank();
console2.log("Test completed: 'No breakglass mask set!' revert verified.");
}

Test Result

Logs:
===== TEST: No breakglass => revert('No breakglass mask set!') =====
Test completed: 'No breakglass mask set!' revert verified.

By implementing these recommendations, the vulnerability can be fully mitigated, ensuring pools remain operable and protected in emergency situations.

Updates

Lead Judging Commences

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

Appeal created

rolando Submitter
7 months ago
n0kto Lead Judge
7 months ago
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.