The issue within the update function
of the MultiFlowPump.sol
contract arises when the function is called twice in the same block, or more generally, at the same timestamp. This can occur in environments where multiple transactions are processed within the same block and can potentially execute contract functions at the same timestamp. Calling the update function
more than once in the same block will lead to skipped update.
An attacker could observe a transaction intending to update the reserves and quickly send a transaction with a higher gas price to ensure it is included in the same block before the observed transaction. The attacker's transaction would update the reserves first. If the legitimate transaction does not have logic to handle the zero deltaTimestamp (because it occurs in the same block), it might skip its intended update.
In the update function, there is a check for deltaTimestamp being zero:
uint256 deltaTimestamp = _getDeltaTimestamp(pumpState.lastTimestamp); // If no time has passed, don't update the pump reserves. if (deltaTimestamp == 0) return;
Here, deltaTimestamp is calculated as the difference between the current block's timestamp (block.timestamp) and pumpState.lastTimestamp, which is the timestamp of the last update. If no time has elapsed (deltaTimestamp == 0), the function returns immediately without making any updates to the reserves. An attacker can manipulate this to return stale data.
`// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/pumps/MultiFlowPump.sol";
contract MultiFlowPumpTest is Test {
MultiFlowPump multiFlowPump;
address wellAddress = address(0x123);
function setUp() public {
multiFlowPump = new MultiFlowPump();
// Setup initial state if necessary, e.g., initializing the well or setting initial reserves
}
function testZeroDeltaTimestampHandling() public {
// Set the initial reserves and perform the first update
uint256[] memory initialReserves = new uint256[](2);
initialReserves[0] = 1000;
initialReserves[1] = 500;
bytes memory data = abi.encode(bytes16(0.1), uint256(3600), MultiFlowPump.CapReservesParameters(...));
vm.startPrank(wellAddress);
multiFlowPump.update(initialReserves, data);
// Move to the next block but keep the same timestamp
vm.roll(block.number + 1);
vm.warp(block.timestamp);
// Attempt to update again with the same timestamp
uint256[] memory newReserves = new uint256[](2);
newReserves[0] = 1100;
newReserves[1] = 550;
multiFlowPump.update(newReserves, data);
// Check if the reserves were updated or not
uint256[] memory updatedReserves = multiFlowPump.readLastCappedReserves(wellAddress, "");
assertEq(updatedReserves[0], 1100, "Reserve 0 should be updated to 1100");
assertEq(updatedReserves[1], 550, "Reserve 1 should be updated to 550");
vm.stopPrank();
}
}`
Important updates might be skipped, leading to outdated or incorrect reserve values being used until the next successful update. missing an update can lead to incorrect calculations or decisions based on stale data.
An attacker could observe a transaction intending to update the reserves and quickly send a transaction with a higher gas price to ensure it is included in the same block before the observed transaction. The attacker's transaction would update the reserves first. If the legitimate transaction does not have logic to handle the zero deltaTimestamp (because it occurs in the same block), it might skip its intended update.
By ensuring that certain updates are skipped, an attacker could manipulate the market conditions to their advantage, either by maintaining favorable conditions for their trades or by creating unfavorable conditions for others.
Manual Review
Implementing a mechanism to queue updates within the same block and process them sequentially.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.