End-of-day option buys vulnerable due to `block.timestamp` dependency
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.
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:
function _checksOnBuy(IStrategy strategy,uint256 period,uint256 relativeStrike,uint256 iv,IvSignerRecovery.SplitSig memory sig) internal view {// ... other checks ...// maturityDate is recalculated here using block.timestampaddress 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:
function _computeMaturityDate(uint256 period) internal view returns (uint32) {uint256 maturityTimestamp = block.timestamp + period; // Uses current block.timestampmaturityTimestamp -= (maturityTimestamp % 1 days); // Rounds down to start of daymaturityTimestamp += 1 days - 1; // Sets to 23:59:59 of that dayreturn uint32(maturityTimestamp);}
Because _computeMaturityDate depends on block.timestamp and rounds the result, the following scenario occurs:
- The off-chain signer service calculates
maturityDatebased 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). - A user submits the
buy()transaction with this signature near the end of Day 1. - Due to network congestion or block timing, the transaction is included in a block mined after midnight (e.g., Day 2, 00:01:00).
- During execution,
_checksOnBuycallsperiod._computeMaturityDate(). This recalculates thematurityDateusing theblock.timestampfrom Day 2. The result will be exactly 24 hours later than thematurityDateoriginally signed (e.g., Day X+1, 23:59:59). - The
_recoverSignerfunction attempts to verify the signature against a data payload containing this new, latermaturityDate. - The signature verification fails because the signed data (with the earlier
maturityDate) does not match the data recalculated on-chain (with the latermaturityDate). The transaction reverts withInvalidIVSigner.
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.
- Sign the Explicit Maturity Date: The off-chain signer should calculate the exact
maturityDatetimestamp (e.g., using the current logic or a fixed daily calculation) and include this explicit timestamp in the data payload that gets signed. - Pass Explicit Date to
buy(): Modify thebuyfunction signature to accept this explicitmaturityDatetimestamp that was signed. - Use Explicit Date in Verification: Modify
_checksOnBuyto pass this explicitmaturityDate(received as an argument) to_recoverSignerinstead of recalculating it usingperiod._computeMaturityDate().
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.