diff --git a/specs/interop/predeploys.md b/specs/interop/predeploys.md
index 922144d5a..9a158c738 100644
--- a/specs/interop/predeploys.md
+++ b/specs/interop/predeploys.md
@@ -14,11 +14,15 @@
   - [`Identifier` Getters](#identifier-getters)
 - [L2ToL2CrossDomainMessenger](#l2tol2crossdomainmessenger)
   - [`relayMessage` Invariants](#relaymessage-invariants)
+  - [`sendExpire` Invariants](#sendexpire-invariants)
+  - [`relayExpire` Invariants](#relayexpire-invariants)
   - [Message Versioning](#message-versioning)
   - [No Native Support for Cross Chain Ether Sends](#no-native-support-for-cross-chain-ether-sends)
   - [Interfaces](#interfaces)
     - [Sending Messages](#sending-messages)
     - [Relaying Messages](#relaying-messages)
+    - [Sending Expired Message Hashes](#sending-expired-message-hashes)
+    - [Relaying Expired Message Hashes](#relaying-expired-message-hashes)
 - [L1Block](#l1block)
   - [Static Configuration](#static-configuration)
   - [Dependency Set](#dependency-set)
@@ -146,6 +150,7 @@ properties about the `_msg`.
 |-------------------|----------------------------------------------|
 | Address           | `0x4200000000000000000000000000000000000023` |
 | `MESSAGE_VERSION` | `uint256(0)`                                 |
+| `EXPIRY_WINDOW`   | `uint256(7200)`                              |
 
 The `L2ToL2CrossDomainMessenger` is a higher level abstraction on top of the `CrossL2Inbox` that
 provides features necessary for secure transfers ERC20 tokens between L2 chains.
@@ -159,6 +164,21 @@ as well as domain binding, ie the executing transaction can only be valid on a s
 - The `_destination` chain id MUST be equal to the local chain id
 - The `CrossL2Inbox` cannot call itself
 
+### `sendExpire` Invariants
+
+- The message MUST have not been successfully relayed
+- The `EXPIRY_WINDOW` MUST have elapsed since the message first failed to be relayed
+- The expired message MUST not have been previously sent back to source
+- The expired message MUST not be relayable after being sent back
+
+### `relayExpire` Invariants
+
+- Only callable by the `CrossL2Inbox`
+- The message source MUST be `block.chainid`
+- The `Identifier.origin` MUST be `address(L2ToL2CrossDomainMessenger)`
+- The `expiredMessages` mapping MUST only contain messages that originated in this chain and failed to be relayed on destination.
+- Already expired messages MUST NOT be relayed.
+
 ### Message Versioning
 
 Versioning is handled in the most significant bits of the nonce, similarly to how it is handled by
@@ -223,6 +243,11 @@ chain. The hash of the message is used for replay protection.
 It is important to ensure that the source chain is in the dependency set of the destination chain, otherwise
 it is possible to send a message that is not playable.
 
+When a message fails to be relayed, only the timestamp at which it
+first failed along with its source chain id are stored. This is
+needed for calculation of the failed message's expiry. The source chain id
+is also required to simplify the function signature of `sendExpire`.
+
 ```solidity
 function relayMessage(uint256 _destination, uint256 _source, uint256 _nonce, address _sender, address _target, bytes memory _message) external payable {
     require(msg.sender == address(CROSS_L2_INBOX));
@@ -245,8 +270,20 @@ function relayMessage(uint256 _destination, uint256 _source, uint256 _nonce, add
        _calldata: _message
     });
 
-    require(success);
-}
+    if (!success) {
+      emit FailedRelayedMessage(messageHash);
+
+      if (failedMessages[messageHash].timestamp == 0) {
+        failedMessages[messageHash] = FailedMessage({timestamp: block.timestamp, sourceChainId: _source});
+      }
+
+      return;
+    }
+
+    successfulMessages[messageHash] = true;
+    delete failedMessages[messageHash];
+    emit RelayedMessage(messageHash);
+};
 ```
 
 Note that the `relayMessage` function is `payable` to enable relayers to earn in the gas paying asset.
@@ -254,6 +291,69 @@ Note that the `relayMessage` function is `payable` to enable relayers to earn in
 To enable cross chain authorization patterns, both the `_sender` and the `_source` MUST be exposed via `public`
 getters.
 
+#### Sending Expired Message Hashes
+
+When expiring a message that failed to be relayed on the destination chain
+to the source chain, it's crucial to ensure the message can only be sent back
+to the `L2ToL2CrossDomainMessenger` contract in its source chain.
+
+This function has no auth, which allows anyone to expire a given message hash.
+The `EXPIRY_WINDOW` variable is added to give the users enough time to replay their
+failed messages and to prevent malicious actors from performing a griefing attack
+by expiring messages upon arrival.
+
+Once the expired message is sent to the source chain, the message on the local chain is set
+as successful in the `successfulMessages` mapping to ensure non-replayability and deleted
+from `failedMessages`. An initiating message is then emitted to `relayExpire`
+
+```solidity
+function sendExpire(bytes32 _expiredHash) external nonReentrant {
+    if (successfulMessages[_expiredHash]) revert MessageAlreadyRelayed();
+
+    (uint256 messageTimestamp, uint256 messageSource) = failedMessages[_expiredHash];
+
+    if (block.timestamp <  messageTimestamp + EXPIRY_WINDOW) revert ExpiryWindowHasNotEnsued();
+
+    delete failedMessages[_expiredHash];
+    successfulMessages[_expiredHash] = true;
+
+    bytes memory data = abi.encodeCall(
+        L2ToL2CrossDomainMessenger.expired,
+        (_expiredHash, messageSource)
+    );
+    emit SentMessage(data);
+}
+```
+
+#### Relaying Expired Message Hashes
+
+When relaying an expired message, only message hashes
+of actual failed messages should be stored, for this we must ensure the origin
+of the log, and caller are all expected contracts.
+
+It's also important to ensure only the hashes of messages that were initiated
+in this chain are accepted.
+
+If all checks have been successful, the message has is stored in the
+`expiredMessages` mapping. This enables smart contracts to read from it and
+check whether a message expired or not, and handle this case accordingly.
+
+```solidity
+function relayExpire(bytes32 _expiredHash, uint256 _messageSource) external {
+    if (_messageSource != block.chainid) revert IncorrectMessageSource();
+    if (expiredMessages[_expiredHash] != 0) revert ExpiredMessageAlreadyRelayed();
+    if (msg.sender != Predeploys.CROSS_L2_INBOX) revert ExpiredMessageCallerNotCrossL2Inbox();
+
+    if (CrossL2Inbox(Predeploys.CROSS_L2_INBOX).origin() != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) {
+        revert CrossL2InboxOriginNotL2ToL2CrossDomainMessenger();
+    }
+
+    expiredMessages[_expiredHash] = block.timestamp;
+
+    emit MessageHashExpired(_expiredHash);
+}
+```
+
 ## L1Block
 
 | Constant            | Value                                        |