QuantAMM

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

Weights Update Vulnerability: Manipulation at the Last Second of the Interval.

Summary

The UpdateWeightRunner contract features a performUpdate function that updates weights based on oracle data. This function operates on an interval-based mechanism to ensure consistent updates. However, a vulnerability in the implementation allows weights to be updated at the last second of the interval. This oversight enables malicious actors—such as miners or MEV bots—to prioritize update transactions in the final second, exploiting the vulnerability for market manipulation. This could result in significant losses for the protocol and associated entities.

Vulnerability Details

see the code snippet below:

UpdateWeightRunner::performUpdate:

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];
// @info: Updates are allowed after the interval has elapsed, not specifically restricted at the last second.
// A MEV bot or miner can front-run the update transaction by 1 second, prioritizing the transaction.
@> 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");
}
}

Key Issues

  1. The performUpdate function allows updates when the condition >= is met, making the last second of the interval vulnerable to manipulation.

  2. Malicious actors can exploit this to prioritize updates that favor them, causing market disruption or protocol losses.

Proof of Concept (PoC)

Scenario:

  1. Alice, a miner, is registered with QuantAMM.

  2. She notices an upcoming update in the mempool that could yield significant profits for her.

  3. She identifies the >= issue in the performUpdate function.

  4. At the last second of the update interval, she prioritizes the update transaction to avoid losses and secure profits.

This scenario highlights how malicious actors can exploit the vulnerability to gain undue advantages, undermining the protocol's integrity.

code proof

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

  2. Paste the following test snippet.

function testUpdatesSuccessfullyatLastSecondOfUpdateInterval() public {
vm.warp(4000); // to discard timestamp under/overflow errors
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,
updateInterval: 60,
lambda: lambda,
epsilonMax: 0.2e18,
absoluteWeightGuardRail: 0.2e18,
maxTradeSizeRatio: 0.2e18,
ruleParameters: new int256[][](),
poolManager: addr2
})
);
vm.stopPrank();
vm.startPrank(addr2);
updateWeightRunner.InitialisePoolLastRunTime(address(mockPool), uint40(block.timestamp));
vm.stopPrank();
vm.warp(block.timestamp + 60);
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 testUpdatesSuccessfullyatLastSecondOfUpdateInterval -vv
  1. see the logs

[⠢] Compiling...
[⠑] Compiling 1 files with Solc 0.8.26
[⠘] Solc 0.8.26 finished in 10.34s
Compiler run successful!
Ran 1 test for test/foundry/UpdateWeightRunner.t.sol:UpdateWeightRunnerTest
[PASS] testUpdatesSuccessfullyatLastSecondOfUpdateInterval() (gas: 928215)
Logs:
inside performUpdate function
timestamp: 4060
lastPoolUpdateRun: 4000
updateInterval: 60
block.timestamp - settings.timingSettings.lastPoolUpdateRun: 60
-----------------------------------------
through _getOracleData function, control comes here
passing before loop
passing before _getOracleData outer loop
inside _getOracleData function:
here in mock oracle
timestamp: 4060
fixedReply: 1000
delay: 3601
data: 1000
timestamp: 459
----------------------------
passing before if check outer loop
passing before inner loop
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 13.93ms (11.59ms CPU time)
Ran 1 test suite in 66.58ms (13.93ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Output:
The test passes, but the logs demonstrate how the update was successfully performed at the last second of the interval, validating the vulnerability. output contains some internal logs also.

Impact

  1. Market manipulation by malicious actors (e.g., miners, MEV bots).

  2. Potential financial losses for the protocol and its users.

  3. Reduced trust in the protocol's mechanisms.

Tools Used

  1. Manual Review

  2. Foundry framework

  3. forge

  4. console

Recommendations

Update the performUpdate function to enforce stricter timing conditions:

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(
- block.timestamp - settings.timingSettings.lastPoolUpdateRun >= settings.timingSettings.updateInterval "Update not allowed");
+ 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");
}
}

This change ensures updates cannot occur precisely at the interval boundary, mitigating the risk of exploitation.

Updates

Lead Judging Commences

n0kto Lead Judge 11 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!