Pre-auditThree days of senior audit time for $1,500, credited in full toward your Zealynx audit.3 audit days, credited toward your auditSee how →
L-26·

End-of-day option buys vulnerable due to `block.timestamp` dependency

Not reportedoptionsleverageblack-scholes
TL;DR

The buy() function in OperationalTreasury.sol allows users to purchase options by providing parameters including a signature (sig) generated off-chain by a trusted signer.

Severity
LOW
Method
MManual review
CAT.

The buy() function in OperationalTreasury.sol allows users to purchase options by providing parameters including a signature (sig) generated off-chain by a trusted signer. This signature covers several parameters, including a maturityDate which is derived from the requested period.

The verification process happens in the internal _checksOnBuy function:

solidity
function _checksOnBuy(
IStrategy strategy,
uint256 period,
uint256 relativeStrike,
uint256 iv,
IvSignerRecovery.SplitSig memory sig
) internal view {
// ... other checks ...
// maturityDate is recalculated here using block.timestamp
address recoveredSigner = _recoverSigner(sig, iv, strategy, period._computeMaturityDate(), relativeStrike);
if (recoveredSigner != strg.setUp.base.signer) revert InvalidIVSigner();
}

The issue lies in how the maturityDate used for signature verification is calculated on-chain via period._computeMaturityDate(). This function, defined in PositionParams.sol, calculates the maturity timestamp based on the current block.timestamp at the time of execution:

solidity
function _computeMaturityDate(uint256 period) internal view returns (uint32) {
uint256 maturityTimestamp = block.timestamp + period; // Uses current block.timestamp
maturityTimestamp -= (maturityTimestamp % 1 days); // Rounds down to start of day
maturityTimestamp += 1 days - 1; // Sets to 23:59:59 of that day
return uint32(maturityTimestamp);
}

Because _computeMaturityDate depends on block.timestamp and rounds the result, the following scenario occurs:

  1. The off-chain signer service calculates maturityDate based on its current timestamp (e.g., Day 1, 23:55:00) and signs the payload including this date (e.g., resulting in Day X, 23:59:59).
  2. A user submits the buy() transaction with this signature near the end of Day 1.
  3. Due to network congestion or block timing, the transaction is included in a block mined after midnight (e.g., Day 2, 00:01:00).
  4. During execution, _checksOnBuy calls period._computeMaturityDate(). This recalculates the maturityDate using the block.timestamp from Day 2. The result will be exactly 24 hours later than the maturityDate originally signed (e.g., Day X+1, 23:59:59).
  5. The _recoverSigner function attempts to verify the signature against a data payload containing this new, later maturityDate.
  6. The signature verification fails because the signed data (with the earlier maturityDate) does not match the data recalculated on-chain (with the later maturityDate). The transaction reverts with InvalidIVSigner.

This effectively creates a Denial of Service (DoS) condition for users attempting to buy options near the end of a UTC day, especially during times of network congestion. Users will experience failed transactions and loss of gas fees because the maturityDate used for verification shifts based on block.timestamp between signing and execution.

Recommendations:

To resolve this, the maturityDate used for signature verification must be consistent between the time of signing and the time of execution. The dependency on block.timestamp during verification for signed data should be removed.

  1. Sign the Explicit Maturity Date: The off-chain signer should calculate the exact maturityDate timestamp (e.g., using the current logic or a fixed daily calculation) and include this explicit timestamp in the data payload that gets signed.
  2. Pass Explicit Date to buy(): Modify the buy function signature to accept this explicit maturityDate timestamp that was signed.
  3. Use Explicit Date in Verification: Modify _checksOnBuy to pass this explicit maturityDate (received as an argument) to _recoverSigner instead of recalculating it using period._computeMaturityDate().
02Section · Report provenance

Report provenance

Published by Pashov Audit Group, with Carlos (Bloqarl) participating as a contractor. The source report states that its fixes review was not fully completed; no final remediation disposition is recorded for this finding. Read the full report.

L-26