QuantAMM

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

Duplicate and Identical Oracles Can Be Set as Happy Path and Backup Oracles — Oracle Centralization Risk

Summary

In the UpdateWeightRunner contract, the setRuleForPool function is responsible for setting rules, oracles, and other configurations for weight calculations and updates. While the function is callable only by a valid pool, it fails to validate the uniqueness of oracles in the happy path and backup arrays. This oversight introduces a significant risk of oracle centralization, potentially jeopardizing the protocol's integrity and resilience.

Vulnerability Details

Code Analysis

Current Implementation of setRuleForPool

function setRuleForPool(IQuantAMMWeightedPool.PoolSettings memory _poolSettings) external {
require(address(rules[msg.sender]) == address(0), "Rule already set");
require(_poolSettings.oracles.length > 0, "Empty oracles array");
require(poolOracles[msg.sender].length == 0, "pool rule already set");
for (uint256 i; i < _poolSettings.oracles.length; ++i) {
require(_poolSettings.oracles[i].length > 0, "Empty oracles array");
for (uint256 j; j < _poolSettings.oracles[i].length; ++j) {
@> // @info: missing redundant oracles check
if (!approvedOracles[_poolSettings.oracles[i][j]]) {
revert("Not approved oracled used");
}
}
}
address[] memory optimisedHappyPathOracles = new address[]();
for (uint256 i; i < _poolSettings.oracles.length; ++i) {
@> // @info: missing redundant oracles check
@> optimisedHappyPathOracles[i] = _poolSettings.oracles[i][0];
}
@> poolOracles[msg.sender] = optimisedHappyPathOracles;
@> poolBackupOracles[msg.sender] = _poolSettings.oracles;
rules[msg.sender] = _poolSettings.rule;
poolRuleSettings[msg.sender] = PoolRuleSettings({
lambda: _poolSettings.lambda,
epsilonMax: _poolSettings.epsilonMax,
absoluteWeightGuardRail: _poolSettings.absoluteWeightGuardRail,
ruleParameters: _poolSettings.ruleParameters,
timingSettings: PoolTimingSettings({updateInterval: _poolSettings.updateInterval, lastPoolUpdateRun: 0}),
poolManager: _poolSettings.poolManager
});
// emit event for easier tracking of rule changes
emit PoolRuleSet(
address(_poolSettings.rule),
_poolSettings.oracles,
_poolSettings.lambda,
_poolSettings.ruleParameters,
_poolSettings.epsilonMax,
_poolSettings.absoluteWeightGuardRail,
_poolSettings.updateInterval,
_poolSettings.poolManager
);
}

Observations

  1. Duplicate Oracle Risk:
    The function does not validate whether the oracles provided in the happy path and backup arrays are unique. This allows the same oracle to be set multiple times, increasing the risk of centralization.

  2. Centralization Consequences:
    Relying on duplicate or identical oracles introduces a single point of failure. If the oracle is compromised, unavailable, or manipulated, the pool's weights may be updated with false or malicious data.

  3. Improper Error Handling:
    The lack of validation not only allows centralization risks but also fails to ensure robustness in oracle selection.

PoC

  1. A pool sets the oracle array with one oracle or several identical oracles.

  2. The oracle becomes compromised, unavailable, or manipulated:

    • Pool weights are updated with incorrect values.

    • The protocol becomes distorted, potentially rendering it useless or inaccessible.

Code Proof

  1. Go to test/foundry/UpdateWeightRunner.t.sol

  2. Paste the following test snippet.

function testSetsRuleForPoolSuccessfully() public {
int256[] memory initialWeights = new int256[]();
initialWeights[0] = 0.0000000005e18;
initialWeights[1] = 0.0000000005e18;
initialWeights[2] = 0;
initialWeights[3] = 0;
// Set initial weights
mockPool.setInitialWeights(initialWeights);
mockPool.setPoolRegistry(9);
vm.startPrank(owner);
updateWeightRunner.setApprovedActionsForPool(address(mockPool), 9);
vm.stopPrank();
int216 fixedValue = 1000;
chainlinkOracle = deployOracle(fixedValue, 3601);
vm.startPrank(owner);
updateWeightRunner.addOracle(OracleWrapper(chainlinkOracle));
vm.stopPrank();
vm.startPrank(address(mockPool));
address[][] memory oracles = new address[][]();
oracles[0] = new address[]();
oracles[1] = new address[]();
oracles[0][0] = address(chainlinkOracle);
oracles[0][1] = address(chainlinkOracle);
oracles[1][0] = address(chainlinkOracle);
oracles[1][1] = address(chainlinkOracle);
uint64[] memory lambda = new uint64[]();
lambda[0] = 0.0000000005e18;
updateWeightRunner.setRuleForPool(
IQuantAMMWeightedPool.PoolSettings({
assets: new IERC20[](0),
rule: IUpdateRule(mockRule),
oracles: oracles,
updateInterval: 1,
lambda: lambda,
epsilonMax: 0.2e18,
absoluteWeightGuardRail: 0.2e18,
maxTradeSizeRatio: 0.2e18,
ruleParameters: new int256[][](),
poolManager: addr2
})
);
vm.stopPrank();
}
  1. execute the following test command... (ensure you're pointing to this location: 2024-12-quantamm/pkg/pool-quantamm)

forge test --mt testSetsRuleForPoolSuccessfully -vv
  1. See the logs

[⠢] Compiling...
[⠒] Compiling 1 files with Solc 0.8.26
[⠑] Solc 0.8.26 finished in 8.31s
Compiler run successful!
Ran 1 test for test/foundry/UpdateWeightRunner.t.sol:UpdateWeightRunnerTest
[PASS] testSetsRuleForPoolSuccessfully() (gas: 880390)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.87ms (748.80µs CPU time)
Ran 1 test suite in 13.20ms (3.87ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Voila, Our test passes.

Impact

  1. Single Point of Failure:
    The protocol becomes vulnerable to downtime or manipulation if the selected oracle fails.

  2. Centralized Control:
    Depending on a single oracle undermines the decentralized nature of the system and places control in the hands of a single entity.

  3. Data Manipulation:
    A centralized oracle can collude with malicious actors or act in bad faith, leading to incorrect weight calculations.

  4. DoS Attack Risks:
    If the oracle becomes unavailable, it can cause a massive denial-of-service (DoS) attack, disrupting the protocol's functionality.


Tools Used

Manual Review

Recommendations

To address this issue, the following steps should be implemented:

  1. Validate Oracle Uniqueness:
    Ensure that all oracles in the happy path and backup arrays are unique to prevent duplication.

  2. Require Multiple Oracles:
    Enforce a minimum number of oracles to ensure redundancy and reduce the risk of centralization.


Proposed Fix

Here’s an updated implementation of the setRuleForPool function:

UpdateWeightRunner::setRuleForPool:

function setRuleForPool(IQuantAMMWeightedPool.PoolSettings memory _poolSettings) external {
require(address(rules[msg.sender]) == address(0), "Rule already set");
require(_poolSettings.oracles.length > 0, "Empty oracles array");
require(poolOracles[msg.sender].length == 0, "pool rule already set");
for (uint256 i; i < _poolSettings.oracles.length; ++i) {
require(_poolSettings.oracles[i].length > 0, "Empty oracles array");
- for (uint256 j; j < _poolSettings.oracles[i].length; ++j) {
+ for(uint256 j; j < _poolSettings.oracles.length; j++) {
+ bool approveChecked;
+ for(uint256 k; k <_poolSettings.oracles[i].length; k++) {
+ for (uint256 l; l < _poolSettings.oracles[i].length; l++) {
+ if (i == j && k == l) {
+ continue;
+ }
+ if (_poolSettings.oracles[i][k] == _poolSettings.oracles[j][l]) {
+ revert("duplicate oracles not allowed");
+ }
+ }
+ if (!approveChecked) {
+ approveChecked = true;
- if (!approvedOracles[_poolSettings.oracles[i][j]]) {
+ if (!approvedOracles[_poolSettings.oracles[i][k]]) {
revert("Not approved oracled used");
}
+ }
+ }
+ }
}
address[] memory optimisedHappyPathOracles = new address[]();
for (uint256 i; i < _poolSettings.oracles.length; ++i) {
optimisedHappyPathOracles[i] = _poolSettings.oracles[i][0];
}
poolOracles[msg.sender] = optimisedHappyPathOracles;
poolBackupOracles[msg.sender] = _poolSettings.oracles;
rules[msg.sender] = _poolSettings.rule;
poolRuleSettings[msg.sender] = PoolRuleSettings({
lambda: _poolSettings.lambda,
epsilonMax: _poolSettings.epsilonMax,
absoluteWeightGuardRail: _poolSettings.absoluteWeightGuardRail,
ruleParameters: _poolSettings.ruleParameters,
timingSettings: PoolTimingSettings({updateInterval: _poolSettings.updateInterval, lastPoolUpdateRun: 0}),
poolManager: _poolSettings.poolManager
});
// emit event for easier tracking of rule changes
emit PoolRuleSet(
address(_poolSettings.rule),
_poolSettings.oracles,
_poolSettings.lambda,
_poolSettings.ruleParameters,
_poolSettings.epsilonMax,
_poolSettings.absoluteWeightGuardRail,
_poolSettings.updateInterval,
_poolSettings.poolManager
);
}

Benefits of the Fix

  1. Enhanced Resilience:
    Prevents reliance on a single oracle, reducing the risk of centralization and single points of failure.

  2. Improved Data Integrity:
    Ensures accurate and reliable weight calculations by using a diverse set of oracles.

  3. Cost Efficiency:
    Avoids gas waste caused by redundant operations or incorrect configurations.

  4. Better Security:
    Mitigates risks of data manipulation and protocol disruption caused by compromised oracles.

Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas / Admin is trusted / Pool creation is trusted / User mistake / Suppositions

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelyhood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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

Give us feedback!