QuantAMM

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

Registered Pools Can Bypass the weights Update Interval and Perform Unauthorized Updates, Causing Fund Loss due to market manipulation.

Summary

The UpdateWeightRunner contract includes a performUpdate function, which updates weights based on oracle data. This function operates using an interval-based approach to ensure regular updates. While most settings and rules for updates are properly configured, the timingSettings are not enforced adequately. Additionally, the InitialisePoolLastRunTime function, intended to set the last update timestamp as a fail-safe, is not integrated effectively into performUpdate. Consequently, registered pools can bypass the update interval and perform unauthorized updates. Let’s explore how this vulnerability occurs.

Vulnerability Details

  1. Initial Setting Issue:

    • In the setRuleForPool function, the lastPoolUpdateRun parameter is initialized to 0 instead of block.timestamp. Setting this value to block.timestamp would provide partial protection against bypassing the update interval.

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) {
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,
@> // @info: lastPoolUpdateRun is set to 0, should be set to block.timestamp
@> 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
);
}
  1. Lack of Validation in performUpdate:

The performUpdate function does not verify if the InitialisePoolLastRunTime function was executed and whether lastPoolUpdateRun is initialized. This oversight allows pools to bypass the update interval.

UpdateWeightRunner::performUpdate:

function performUpdate(address _pool) public {
//Main external access point to trigger an update
@> // @info: missing check to check lastPoolUpdateRun was initialised and/or not 0
address rule = address(rules[_pool]);
require(rule != address(0), "Pool not registered");
PoolRuleSettings memory settings = poolRuleSettings[_pool];
require(
block.timestamp - settings.timingSettings.lastPoolUpdateRun >= settings.timingSettings.updateInterval,
"Update not allowed"
);
uint256 poolRegistryEntry = approvedPoolActions[_pool];
if (poolRegistryEntry & MASK_POOL_PERFORM_UPDATE > 0) {
_performUpdateAndGetData(_pool, settings);
// emit event for easier tracking of updates and to allow for easier querying of updates
emit UpdatePerformed(msg.sender, _pool);
} else {
revert("Pool not approved to perform update");
}
}
  1. Insufficient Integration of InitialisePoolLastRunTime:

The InitialisePoolLastRunTime function initializes lastPoolUpdateRun. However, the performUpdate function lacks enforcement of this initialization.

UpdateWeightRunner::InitialisePoolLastRunTime:

function InitialisePoolLastRunTime(address _poolAddress, uint40 _time) external {
uint256 poolRegistryEntry = approvedPoolActions[_poolAddress];
//current breakglass settings allow pool creator trigger. This is subject to review
if (poolRegistryEntry & MASK_POOL_OWNER_UPDATES > 0) {
require(msg.sender == poolRuleSettings[_poolAddress].poolManager, "ONLYMANAGER");
} else if (poolRegistryEntry & MASK_POOL_QUANTAMM_ADMIN_UPDATES > 0) {
require(msg.sender == quantammAdmin, "ONLYADMIN");
} else {
revert("No permission to set last run time");
}
// @info: okay to not sanitize the _time, therefore it would be admin or poolmanager's input validation responsibility
@> // lastPoolUpdateRun initialisation
@> poolRuleSettings[_poolAddress].timingSettings.lastPoolUpdateRun = _time;
emit PoolLastRunSet(_poolAddress, _time);
}

PoC

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

  2. Paste the following test snippet.

function testUpdatesSuccessfullyAfterUpdateIntervalByPass() public {
// set to avoid chainlink delay and timestamp over/underflow issue
// in real world block.timestamp always has a value greater than 1970
vm.warp(4000);
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[0][0] = address(chainlinkOracle);
uint64[] memory lambda = new uint64[]();
lambda[0] = 0.0000000005e18;
updateWeightRunner.setRuleForPool(
IQuantAMMWeightedPool.PoolSettings({
assets: new IERC20[](0),
rule: IUpdateRule(mockRule),
oracles: oracles,
// we sets interval here
updateInterval: 60,
lambda: lambda,
epsilonMax: 0.2e18,
absoluteWeightGuardRail: 0.2e18,
maxTradeSizeRatio: 0.2e18,
ruleParameters: new int256[][](),
poolManager: addr2
})
);
vm.stopPrank();
// we commented out the initialisation step
// vm.startPrank(addr2);
// updateWeightRunner.InitialisePoolLastRunTime(address(mockPool), 1);
// vm.stopPrank();
// vm.warp(block.timestamp + 10000000);
mockRule.CalculateNewWeights(
initialWeights, new int256[](0), address(mockPool), new int256[][](), new uint64[](0), 0.2e18, 0.2e18
);
updateWeightRunner.performUpdate(address(mockPool));
uint40 timeNow = uint40(block.timestamp);
assertEq(updateWeightRunner.getPoolRuleSettings(address(mockPool)).timingSettings.lastPoolUpdateRun, timeNow);
assertTrue(mockRule.CalculateNewWeightsCalled());
}
  1. execute the following test command... (ensure you're pointing to this location: 2024-12-quantamm/pkg/pool-quantamm)

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

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/foundry/UpdateWeightRunner.t.sol:UpdateWeightRunnerTest
[PASS] testUpdatesSuccessfullyAfterUpdateIntervalByPass() (gas: 919282)
Logs:
through _getOracleData function, control comes here
passing before loop
passing before _getOracleData outer loop
inside _getOracleData function:
here in mock oracle
timestamp: 4000
fixedReply: 1000
delay: 3601
1000
timestamp: 399
passing before if check outer loop
passing before inner loop
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.52ms (3.20ms CPU time)
Ran 1 test suite in 24.53ms (7.52ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

You'll see some unnecessay log comments, don't worry i performed some console logging to trace block.timestamp over/underflow error.

but Guess what our test passed successfully and i think it's enough to prove my point.

Impact

This vulnerability can cause significant financial losses for the protocol due to unauthorized weight updates. Potential impacts include:

  1. Market manipulation by malicious actors.

  2. Disruption in the fairness of automated market-making processes.

  3. Compromised oracle reliability due to frequent or unplanned updates.

Tools Used

  1. Manual code review.

  2. Foundry’s Forge testing framework.

  3. Console.sol for debugging and logging.

Recommendations

  1. We can set lastPoolUpdateRun to block.timestamp in setRuleForPool function to have a semi-protection against interval bypass.

  2. Second, we can implement a check in the performUpdate function to ensure that lastPoolUpdateRun is initialised and not 0.

  • First Solution:

.
.
...
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}),
+ timingSettings: PoolTimingSettings({updateInterval: _poolSettings.updateInterval, lastPoolUpdateRun: block.timestamp}),
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
);
}
.
.
...
function performUpdate(address _pool) public {
//Main external access point to trigger an update
address rule = address(rules[_pool]);
require(rule != address(0), "Pool not registered");
PoolRuleSettings memory settings = poolRuleSettings[_pool];
+ require(settings.timingSettings.lastPoolUpdateRun > 0 && settings.timingSettings.lastPoolUpdateRun <= type(uint40).max);
require(
block.timestamp - settings.timingSettings.lastPoolUpdateRun >= settings.timingSettings.updateInterval,
"Update not allowed"
);
uint256 poolRegistryEntry = approvedPoolActions[_pool];
if (poolRegistryEntry & MASK_POOL_PERFORM_UPDATE > 0) {
_performUpdateAndGetData(_pool, settings);
// emit event for easier tracking of updates and to allow for easier querying of updates
emit UpdatePerformed(msg.sender, _pool);
} else {
revert("Pool not approved to perform update");
}
}
  • 2nd solution

+ bool poolLastRunTimeInitialised;
.
.
...
function performUpdate(address _pool) public {
//Main external access point to trigger an update
+ require(poolLastRunTimeInitialised, " pool last runtime not initialised");
address rule = address(rules[_pool]);
require(rule != address(0), "Pool not registered");
PoolRuleSettings memory settings = poolRuleSettings[_pool];
require(
block.timestamp - settings.timingSettings.lastPoolUpdateRun >= settings.timingSettings.updateInterval,
"Update not allowed"
);
uint256 poolRegistryEntry = approvedPoolActions[_pool];
if (poolRegistryEntry & MASK_POOL_PERFORM_UPDATE > 0) {
_performUpdateAndGetData(_pool, settings);
+ poolLastRunTimeInitialised = false;
// emit event for easier tracking of updates and to allow for easier querying of updates
emit UpdatePerformed(msg.sender, _pool);
} else {
revert("Pool not approved to perform update");
}
}
.
.
...
function InitialisePoolLastRunTime(address _poolAddress, uint40 _time) external {
uint256 poolRegistryEntry = approvedPoolActions[_poolAddress];
//current breakglass settings allow pool creator trigger. This is subject to review
if (poolRegistryEntry & MASK_POOL_OWNER_UPDATES > 0) {
require(msg.sender == poolRuleSettings[_poolAddress].poolManager, "ONLYMANAGER");
} else if (poolRegistryEntry & MASK_POOL_QUANTAMM_ADMIN_UPDATES > 0) {
require(msg.sender == quantammAdmin, "ONLYADMIN");
} else {
revert("No permission to set last run time");
}
poolRuleSettings[_poolAddress].timingSettings.lastPoolUpdateRun = _time;
+ poolLastRunTimeInitialised = true;
emit PoolLastRunSet(_poolAddress, _time);
}

These changes will mitigate the vulnerability and prevent bypassing the update interval, ensuring the protocol remains secure.

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!