The contract defines two mutually exclusive time gates around roundEnd: buyKeys requires block.timestamp < roundEnd (require(block.timestamp < roundEnd, "Round ended, call endRound()")), while endRound requires block.timestamp >= roundEnd (require(block.timestamp >= roundEnd, "Round not over yet")) and has no access control (function endRound() external). As a result, in any block whose block.timestamp is at or after roundEnd, endRound() can execute successfully (assuming lastBuyer != address(0)), and any buyKeys() transaction executed later in the same block will deterministically revert due to failing the time check. The contract does not include a “grace period,” a “last-buyer-only finalization window,” or any ordering constraint that prioritizes a pending buyKeys over endRound once the timestamp boundary is crossed; therefore, the outcome at expiry is sensitive to transaction ordering within the first block with block.timestamp >= roundEnd.
At the expiry boundary, a buyKeys() transaction that is not mined before the first block with block.timestamp >= roundEnd will revert, while endRound() remains callable by any address in that same block.
Mitigation/ Suggestion
Require endRound() to be callable only by lastBuyer for an initial post-expiry window (e.g., first N seconds) and allow anyone only after that window elapses.