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) {
@>
if (!approvedOracles[_poolSettings.oracles[i][j]]) {
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 PoolRuleSet(
address(_poolSettings.rule),
_poolSettings.oracles,
_poolSettings.lambda,
_poolSettings.ruleParameters,
_poolSettings.epsilonMax,
_poolSettings.absoluteWeightGuardRail,
_poolSettings.updateInterval,
_poolSettings.poolManager
);
}
Observations
-
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.
-
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.
-
Improper Error Handling:
The lack of validation not only allows centralization risks but also fails to ensure robustness in oracle selection.
PoC
A pool sets the oracle array with one oracle or several identical oracles.
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
Go to test/foundry/UpdateWeightRunner.t.sol
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;
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();
}
execute the following test command... (ensure you're pointing to this location: 2024-12-quantamm/pkg/pool-quantamm)
forge test --mt testSetsRuleForPoolSuccessfully -vv
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
-
Single Point of Failure:
The protocol becomes vulnerable to downtime or manipulation if the selected oracle fails.
-
Centralized Control:
Depending on a single oracle undermines the decentralized nature of the system and places control in the hands of a single entity.
-
Data Manipulation:
A centralized oracle can collude with malicious actors or act in bad faith, leading to incorrect weight calculations.
-
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:
-
Validate Oracle Uniqueness:
Ensure that all oracles in the happy path and backup arrays are unique to prevent duplication.
-
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
-
Enhanced Resilience:
Prevents reliance on a single oracle, reducing the risk of centralization and single points of failure.
-
Improved Data Integrity:
Ensures accurate and reliable weight calculations by using a diverse set of oracles.
-
Cost Efficiency:
Avoids gas waste caused by redundant operations or incorrect configurations.
-
Better Security:
Mitigates risks of data manipulation and protocol disruption caused by compromised oracles.