diff --git a/src/FilecoinPayV1.sol b/src/FilecoinPayV1.sol index 0d1dc78..5f94c43 100644 --- a/src/FilecoinPayV1.sol +++ b/src/FilecoinPayV1.sol @@ -1596,6 +1596,7 @@ contract FilecoinPayV1 is ReentrancyGuard { rail.settledUpTo = 0; rail.endEpoch = 0; rail.commissionRateBps = 0; + rail.rateChangeQueue.clearEmpty(); } function updateOperatorRateUsage(OperatorApproval storage approval, uint256 oldRate, uint256 newRate) internal { diff --git a/src/RateChangeQueue.sol b/src/RateChangeQueue.sol index d8a3c8e..b071404 100644 --- a/src/RateChangeQueue.sol +++ b/src/RateChangeQueue.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.27; library RateChangeQueue { + error EmptyQueue(); + struct RateChange { // The payment rate to apply uint256 rate; @@ -18,33 +20,38 @@ library RateChangeQueue { queue.changes.push(RateChange(rate, untilEpoch)); } - function dequeue(Queue storage queue) internal returns (RateChange memory) { + function dequeue(Queue storage queue) internal returns (RateChange memory change) { RateChange[] storage c = queue.changes; - require(queue.head < c.length, "Queue is empty"); - RateChange memory change = c[queue.head]; - delete c[queue.head]; - - if (isEmpty(queue)) { - queue.head = 0; - // The array is already empty, waste no time zeroing it. - assembly { - sstore(c.slot, 0) - } - } else { - queue.head++; + require(queue.head < c.length, EmptyQueue()); + unchecked { + change = c[queue.head]; + delete c[queue.head++]; } + } - return change; + // Clears the storage of the Queue + // If the queue isEmpty, all queue storage will be cleared + // Otherwise, the queue is functionally emptied but pending RateChange are not cleared from storage + function clearEmpty(Queue storage queue) internal { + queue.head = 0; + RateChange[] storage c = queue.changes; + assembly ("memory-safe") { + sstore(c.slot, 0) + } } - function peek(Queue storage queue) internal view returns (RateChange memory) { - require(queue.head < queue.changes.length, "Queue is empty"); - return queue.changes[queue.head]; + function peek(Queue storage queue) internal view returns (RateChange memory change) { + require(queue.head < queue.changes.length, EmptyQueue()); + unchecked { + change = queue.changes[queue.head]; + } } - function peekTail(Queue storage queue) internal view returns (RateChange memory) { - require(queue.head < queue.changes.length, "Queue is empty"); - return queue.changes[queue.changes.length - 1]; + function peekTail(Queue storage queue) internal view returns (RateChange memory change) { + require(queue.head < queue.changes.length, EmptyQueue()); + unchecked { + change = queue.changes[queue.changes.length - 1]; + } } function isEmpty(Queue storage queue) internal view returns (bool) { diff --git a/test/RateChangeQueue.t.sol b/test/RateChangeQueue.t.sol index 34c2adb..31d209e 100644 --- a/test/RateChangeQueue.t.sol +++ b/test/RateChangeQueue.t.sol @@ -117,6 +117,11 @@ contract RateChangeQueueTest is Test { // Queue should now be empty assertTrue(RateChangeQueue.isEmpty(queue())); assertEq(RateChangeQueue.size(queue()), 0); + + // Empty queue should have reset to head = 0 + queue().clearEmpty(); + assertEq(queue().head, 0); + assertEq(queue().changes.length, 0); } /// forge-config: default.allow_internal_expect_revert = true @@ -124,7 +129,7 @@ contract RateChangeQueueTest is Test { createEmptyQueue(); // Test dequeue on empty queue - vm.expectRevert("Queue is empty"); + vm.expectRevert(abi.encodeWithSelector(RateChangeQueue.EmptyQueue.selector)); RateChangeQueue.dequeue(queue()); } @@ -133,7 +138,7 @@ contract RateChangeQueueTest is Test { createEmptyQueue(); // Test peek on empty queue - vm.expectRevert("Queue is empty"); + vm.expectRevert(abi.encodeWithSelector(RateChangeQueue.EmptyQueue.selector)); RateChangeQueue.peek(queue()); } @@ -142,7 +147,7 @@ contract RateChangeQueueTest is Test { createEmptyQueue(); // Test peekTail on empty queue - vm.expectRevert("Queue is empty"); + vm.expectRevert(abi.encodeWithSelector(RateChangeQueue.EmptyQueue.selector)); RateChangeQueue.peekTail(queue()); }