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 {
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 UpdatePerformed(msg.sender, _pool);
} else {
revert("Pool not approved to perform update");
}
}
Key Issues
The performUpdate function allows updates when the condition >= is met, making the last second of the interval vulnerable to manipulation.
Malicious actors can exploit this to prioritize updates that favor them, causing market disruption or protocol losses.
Proof of Concept (PoC)
Scenario:
Alice, a miner, is registered with QuantAMM.
She notices an upcoming update in the mempool that could yield significant profits for her.
She identifies the >= issue in the performUpdate function.
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
Go to test/foundry/UpdateWeightRunner.t.sol
Paste the following test snippet.
function testUpdatesSuccessfullyatLastSecondOfUpdateInterval() public {
vm.warp(4000);
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[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());
}
execute the following test command... (ensure you're pointing to this location: 2024-12-quantamm/pkg/pool-quantamm)
forge test --mt testUpdatesSuccessfullyatLastSecondOfUpdateInterval -vv
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
Market manipulation by malicious actors (e.g., miners, MEV bots).
Potential financial losses for the protocol and its users.
Reduced trust in the protocol's mechanisms.
Tools Used
Manual Review
Foundry framework
forge
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.