Skip to content

Commit ecf6521

Browse files
committed
fix: guard against updateat timestampin future
1 parent 40c41e2 commit ecf6521

File tree

4 files changed

+61
-14
lines changed

4 files changed

+61
-14
lines changed

contracts/contracts/sapphire/CrossChainPaymaster.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,8 @@ contract CrossChainPaymaster is
361361
*/
362362
function _decodeRlpUint(bytes memory rlp) internal pure returns (uint256) {
363363
bytes memory b = rlp.toRLPItem().readBytes();
364+
// Guard: uint256 can only hold 32 bytes max; larger values cause overflow
365+
if (b.length > 32) revert InvalidEvent();
364366
uint256 number;
365367
for (uint256 i = 0; i < b.length; i++) {
366368
number = number + uint256(uint8(b[i])) * (2 ** (8 * (b.length - (i + 1))));
@@ -390,7 +392,8 @@ contract CrossChainPaymaster is
390392
// Validate token price
391393
if (tPrice <= 0) revert InvalidPrice(tPrice);
392394
if (tAnsweredIn < tRound) revert StalePrice(0, 0);
393-
if (tUpdated == 0 || (block.timestamp > tUpdated && block.timestamp - tUpdated > stalenessThreshold)) revert StalePrice(tUpdated, stalenessThreshold);
395+
if (tUpdated > block.timestamp) revert FuturePriceTimestamp(tUpdated, block.timestamp);
396+
if (tUpdated == 0 || block.timestamp - tUpdated > stalenessThreshold) revert StalePrice(tUpdated, stalenessThreshold);
394397

395398
// Get ROSE/USD price (single aggregated feed from ROFL oracle)
396399
AggregatorV3Interface roseFeed = AggregatorV3Interface(roseUsdFeed);
@@ -399,7 +402,8 @@ contract CrossChainPaymaster is
399402
// Validate ROSE price
400403
if (rPrice <= 0) revert InvalidPrice(rPrice);
401404
if (rAnsweredIn < rRound) revert StalePrice(0, 0);
402-
if (rUpdated == 0 || (block.timestamp > rUpdated && block.timestamp - rUpdated > stalenessThreshold)) revert StalePrice(rUpdated, stalenessThreshold);
405+
if (rUpdated > block.timestamp) revert FuturePriceTimestamp(rUpdated, block.timestamp);
406+
if (rUpdated == 0 || block.timestamp - rUpdated > stalenessThreshold) revert StalePrice(rUpdated, stalenessThreshold);
403407

404408
uint8 tokenDec = tokenDecimals[token];
405409
if (tokenDec == 0) tokenDec = 18;

contracts/contracts/sapphire/interfaces/ICrossChainPaymaster.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ interface ICrossChainPaymaster {
7979
error InvalidPriceFeed();
8080
error NoPriceFeedForToken(address token);
8181
error StalePrice(uint256 timestamp, uint256 threshold);
82+
error FuturePriceTimestamp(uint256 priceTimestamp, uint256 blockTimestamp);
8283
error InvalidPrice(int256 price);
8384
error ChainDisabled(uint256 chainId);
8485
error VaultNotAuthorized(uint256 chainId, address vault);

paymaster-relayer/paymaster_relayer/event_processor.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,15 @@ async def process_matched_payment(self, payment_event: PaymentEvent) -> None:
215215
payment_event, paymaster_address
216216
)
217217

218+
if tx_hash is None:
219+
logger.warning(
220+
f"Proof submission failed for tx {payment_event.tx_hash}, "
221+
"will retry on next cycle"
222+
)
223+
return
224+
218225
logger.info(f"Proof submitted successfully: {tx_hash}")
219-
# Remove from pending structures
226+
# Remove from pending structures only on success
220227
block_payments = self.pending_payments.get(payment_event.block_number, [])
221228
if payment_event in block_payments:
222229
block_payments.remove(payment_event)

paymaster-relayer/paymaster_relayer/proof_manager.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
for cross-chain message verification using the Hashi protocol format.
66
"""
77

8+
import asyncio
89
import logging
910
from typing import TYPE_CHECKING, Any
1011

@@ -23,6 +24,10 @@
2324

2425
logger = logging.getLogger(__name__)
2526

27+
FUTURE_PRICE_TIMESTAMP_ERROR_B64 = "4B8j6Q"
28+
FUTURE_PRICE_RETRY_DELAY = 6 # seconds to wait before retry
29+
FUTURE_PRICE_MAX_RETRIES = 3 # maximum retry attempts
30+
2631

2732
class ProofManager:
2833
"""Handles proof generation and submission for cross-chain messages."""
@@ -176,16 +181,19 @@ async def generate_proof(self, payment_event: PaymentEvent) -> list[Any]:
176181
)
177182
return proof
178183

179-
async def submit_proof(self, proof: list[Any], paymaster_address: str) -> str:
184+
async def submit_proof(self, proof: list[Any], paymaster_address: str) -> str | None:
180185
"""
181186
Submit proof to CrossChainPaymaster contract.
182187
188+
Includes retry logic for FuturePriceTimestamp errors, which occur when
189+
the price oracle's timestamp is slightly ahead of the block timestamp.
190+
183191
Args:
184192
proof: The generated proof array
185193
paymaster_address: Address of the CrossChainPaymaster contract
186194
187195
Returns:
188-
Transaction hash of the submission
196+
Transaction hash of the submission, or None if all retries failed
189197
"""
190198
logger.info(f"Submitting proof to CrossChainPaymaster at {paymaster_address}")
191199

@@ -210,7 +218,7 @@ async def submit_proof(self, proof: list[Any], paymaster_address: str) -> str:
210218
)
211219

212220
if self.rofl_util:
213-
# ROFL mode: build transaction for rofl_util
221+
# ROFL mode: build transaction for rofl_util with retry logic
214222
tx_params: TxParams = {
215223
"from": "0x0000000000000000000000000000000000000000", # ROFL will override
216224
"gas": 3000000,
@@ -220,14 +228,41 @@ async def submit_proof(self, proof: list[Any], paymaster_address: str) -> str:
220228
tx_data = contract.functions.processPayment(
221229
receipt_proof_struct
222230
).build_transaction(tx_params)
223-
success = await self.rofl_util.submit_tx(tx_data)
224-
if success:
225-
logger.info("Proof submitted successfully via ROFL")
226-
# Return a success indicator since ROFL doesn't provide tx hash
227-
return "ROFL_SUBMITTED"
228-
else:
229-
logger.error("Failed to submit proof via ROFL")
230-
raise Exception("ROFL submission failed")
231+
232+
# Retry loop for FuturePriceTimestamp errors
233+
for attempt in range(FUTURE_PRICE_MAX_RETRIES):
234+
try:
235+
success = await self.rofl_util.submit_tx(tx_data)
236+
if success:
237+
logger.info("Proof submitted successfully via ROFL")
238+
return "ROFL_SUBMITTED"
239+
else:
240+
logger.error("Failed to submit proof via ROFL (no success)")
241+
return None
242+
except Exception as e:
243+
error_msg = str(e)
244+
if FUTURE_PRICE_TIMESTAMP_ERROR_B64 in error_msg:
245+
remaining = FUTURE_PRICE_MAX_RETRIES - attempt - 1
246+
if remaining > 0:
247+
logger.warning(
248+
f"FuturePriceTimestamp error (oracle ahead of block), "
249+
f"retrying in {FUTURE_PRICE_RETRY_DELAY}s... "
250+
f"({remaining} retries left)"
251+
)
252+
await asyncio.sleep(FUTURE_PRICE_RETRY_DELAY)
253+
continue
254+
else:
255+
logger.error(
256+
f"FuturePriceTimestamp error persisted after "
257+
f"{FUTURE_PRICE_MAX_RETRIES} attempts, giving up: {error_msg}"
258+
)
259+
return None
260+
else:
261+
# Non-retryable error
262+
logger.error(f"ROFL submission failed with error: {error_msg}")
263+
return None
264+
265+
return None
231266
else:
232267
# Local mode
233268
tx_hash = contract.functions.processPayment(receipt_proof_struct).transact(

0 commit comments

Comments
 (0)