@@ -32,6 +32,7 @@ struct StagedOperation {
3232}
3333
3434contract SecurityPoolOracleCoordinator {
35+ uint256 public constant MAX_PENDING_SETTLEMENT_OPERATIONS = 4 ;
3536 uint256 public pendingReportId;
3637 uint256 public pendingOperationSlotId;
3738 uint256 public lastSettlementTimestamp;
@@ -71,9 +72,9 @@ contract SecurityPoolOracleCoordinator {
7172 );
7273 event ExecutedStagedOperation (uint256 operationId , OperationType operation , bool success , string errorMessage );
7374
74- // This is not a FIFO queue. We keep an append-only operation record and a single
75- // pending slot that settlement can auto-execute once a fresh oracle price arrives.
76- // Active-operation paging is newest-first so UI previews remain stable after manual
75+ // This is not a FIFO queue. We keep append-only operation records plus a bounded
76+ // pending settlement list that auto-executes once a fresh oracle price arrives.
77+ // Active-operation paging is newest-first so UI previews remain stable after
7778 // execution removes older entries from the set.
7879 uint256 public stagedOperationCounter;
7980 mapping (uint256 => StagedOperation) public stagedOperations;
@@ -82,6 +83,7 @@ contract SecurityPoolOracleCoordinator {
8283 mapping (uint256 => uint256 ) private olderActiveStagedOperationIds;
8384 mapping (uint256 => uint256 ) private newerActiveStagedOperationIds;
8485 mapping (uint256 => bool ) private isActiveStagedOperation;
86+ uint256 [] private pendingSettlementOperationIds;
8587
8688 constructor (
8789 OpenOracle _openOracle ,
@@ -138,9 +140,17 @@ contract SecurityPoolOracleCoordinator {
138140 }
139141
140142 function getRequestPriceEthCost () public view returns (uint256 ) {
141- uint256 ethCost = block .basefee * 4 * (gasConsumedSettlement + gasConsumedOpenOracleReportPrice) + 101 ;
143+ uint256 ethCost =
144+ block .basefee * 4 * (getSettlementCallbackGasLimit () + gasConsumedOpenOracleReportPrice) + 101 ;
142145 return ethCost;
143146 }
147+
148+ function getSettlementCallbackGasLimit () public view returns (uint32 ) {
149+ uint256 callbackGasLimit = uint256 (gasConsumedSettlement) * MAX_PENDING_SETTLEMENT_OPERATIONS;
150+ require (callbackGasLimit <= type (uint32 ).max, 'settlement gas too high ' );
151+ return uint32 (callbackGasLimit);
152+ }
153+
144154 function requestPrice () public payable {
145155 require (pendingReportId == 0 , 'Already pending request ' );
146156 uint256 ethCost = getRequestPriceEthCost ();
@@ -161,7 +171,7 @@ contract SecurityPoolOracleCoordinator {
161171 disputeDelay: disputeDelay,
162172 protocolFee: protocolFee,
163173 token2Address: address (weth), // address of token2 in the oracle report instance
164- callbackGasLimit: gasConsumedSettlement , // gas the settlement callback must use
174+ callbackGasLimit: getSettlementCallbackGasLimit () , // gas the settlement callback must use
165175 feePercentage: feePercentage,
166176 multiplier: multiplier,
167177 timeType: timeType,
@@ -211,19 +221,15 @@ contract SecurityPoolOracleCoordinator {
211221 price /
212222 ORACLE_BUDGET_BPS;
213223 emit PriceReported (reportId, lastPrice);
214- if (pendingOperationSlotId != 0 ) {
215- // TODO we maybe should allow executing couple operations?
216- uint256 operationId = pendingOperationSlotId ;
224+ if (pendingSettlementOperationIds. length != 0 ) {
225+ uint256 [] memory operationIds = pendingSettlementOperationIds;
226+ delete pendingSettlementOperationIds ;
217227 pendingOperationSlotId = 0 ;
218- StagedOperation memory stagedOperation = stagedOperations[operationId];
219- if (stagedOperation.initiatorVault == address (0 )) return ;
220- if (block .timestamp > stagedOperation.queuedAt + settlementTime + stagedOperation.validForSeconds) {
221- _consumeActiveStagedOperation (operationId);
222- stagedOperations[operationId].initiatorVault = address (0 );
223- emit ExecutedStagedOperation (operationId, stagedOperation.operation, false , 'staged operation expired ' );
224- return ;
228+ for (uint256 index = 0 ; index < operationIds.length ; index++ ) {
229+ if (stagedOperations[operationIds[index]].initiatorVault != address (0 )) {
230+ executeStagedOperation (operationIds[index]);
231+ }
225232 }
226- executeStagedOperation (operationId);
227233 }
228234 }
229235
@@ -289,25 +295,24 @@ contract SecurityPoolOracleCoordinator {
289295 emit StagedOperationQueued (operationId, operation, msg .sender , targetVault, amount, false );
290296 executeStagedOperation (operationId);
291297 // no cost when price is valid
292- } else if (pendingReportId == 0 && pendingOperationSlotId == 0 ) {
293- pendingOperationSlotId = operationId;
294- emit StagedOperationQueued (operationId, operation, msg .sender , targetVault, amount, true );
295- uint256 ethCost = getRequestPriceEthCost ();
296- require (msg .value >= ethCost, 'not enough eth to request price ' );
297- retained += ethCost;
298- // Forward exactly ethCost to requestPrice to create the report
299- this .requestPrice { value: ethCost }();
300- } else if (pendingReportId == 0 ) {
301- emit StagedOperationQueued (operationId, operation, msg .sender , targetVault, amount, false );
302- } else if (pendingOperationSlotId == 0 ) {
303- pendingOperationSlotId = operationId;
304- emit StagedOperationQueued (operationId, operation, msg .sender , targetVault, amount, true );
305298 } else {
306- emit StagedOperationQueued (operationId, operation, msg .sender , targetVault, amount, false );
307- // This is intentional: only one staged operation is marked as the auto-execute
308- // pending slot for the next fresh oracle report. Additional operations are still
309- // recorded and can be executed manually via executeStagedOperation once the price
310- // becomes valid again.
299+ bool shouldRequestPrice = pendingReportId == 0 && pendingSettlementOperationIds.length == 0 ;
300+ bool isPendingSettlementOperationId = _trackPendingSettlementOperation (operationId);
301+ emit StagedOperationQueued (
302+ operationId,
303+ operation,
304+ msg .sender ,
305+ targetVault,
306+ amount,
307+ isPendingSettlementOperationId
308+ );
309+ if (shouldRequestPrice && isPendingSettlementOperationId) {
310+ uint256 ethCost = getRequestPriceEthCost ();
311+ require (msg .value >= ethCost, 'not enough eth to request price ' );
312+ retained += ethCost;
313+ // Forward exactly ethCost to requestPrice to create the report
314+ this .requestPrice { value: ethCost }();
315+ }
311316 }
312317
313318 // Refund the excess of msg.value that was not retained
@@ -471,6 +476,7 @@ contract SecurityPoolOracleCoordinator {
471476 }
472477
473478 function _consumeStagedOperation (uint256 operationId ) private {
479+ _consumePendingSettlementOperation (operationId);
474480 _consumeActiveStagedOperation (operationId);
475481 stagedOperations[operationId].initiatorVault = address (0 );
476482 }
@@ -483,6 +489,14 @@ contract SecurityPoolOracleCoordinator {
483489 return activeStagedOperationCount;
484490 }
485491
492+ function getPendingSettlementOperationCount () public view returns (uint256 ) {
493+ return pendingSettlementOperationIds.length ;
494+ }
495+
496+ function getPendingSettlementOperationIds () public view returns (uint256 [] memory ) {
497+ return pendingSettlementOperationIds;
498+ }
499+
486500 function getActiveStagedOperations (
487501 uint256 startIndex ,
488502 uint256 count
@@ -516,6 +530,28 @@ contract SecurityPoolOracleCoordinator {
516530 latestActiveStagedOperationId = operationId;
517531 }
518532
533+ function _trackPendingSettlementOperation (uint256 operationId ) private returns (bool ) {
534+ if (pendingSettlementOperationIds.length >= MAX_PENDING_SETTLEMENT_OPERATIONS) return false ;
535+ pendingSettlementOperationIds.push (operationId);
536+ if (pendingOperationSlotId == 0 ) {
537+ pendingOperationSlotId = operationId;
538+ }
539+ return true ;
540+ }
541+
542+ function _consumePendingSettlementOperation (uint256 operationId ) private {
543+ uint256 operationCount = pendingSettlementOperationIds.length ;
544+ for (uint256 index = 0 ; index < operationCount; index++ ) {
545+ if (pendingSettlementOperationIds[index] != operationId) continue ;
546+ for (uint256 shiftIndex = index + 1 ; shiftIndex < operationCount; shiftIndex++ ) {
547+ pendingSettlementOperationIds[shiftIndex - 1 ] = pendingSettlementOperationIds[shiftIndex];
548+ }
549+ pendingSettlementOperationIds.pop ();
550+ pendingOperationSlotId = pendingSettlementOperationIds.length == 0 ? 0 : pendingSettlementOperationIds[0 ];
551+ return ;
552+ }
553+ }
554+
519555 function _consumeActiveStagedOperation (uint256 operationId ) private {
520556 if (! isActiveStagedOperation[operationId]) return ;
521557 uint256 olderOperationId = olderActiveStagedOperationIds[operationId];
0 commit comments