|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.4; |
| 3 | + |
| 4 | +/// @notice This library provides encoding and validation for finality parameters used in cross-chain transfers. |
| 5 | +/// @dev this codec supports all the bit flags, even though some might not be assigned any meaning yet. This is |
| 6 | +/// intentional to allow for future flexibility. |
| 7 | +library FinalityCodec { |
| 8 | + error InvalidBlockDepth(uint16 requestedDepth, uint16 maxDepth); |
| 9 | + /// @notice Requested finality must be exactly one mode: any of the flag bits or a block depth with no upper flag bits. |
| 10 | + /// It cannot combine a flag with a block depth. |
| 11 | + error InvalidRequestedFinality(bytes2 encodedFinality); |
| 12 | + |
| 13 | + /// @notice The block depth is stored in the lower 10 bits, leaving the upper 6 bits for flags. This allows for a |
| 14 | + /// maximum block depth of 1023, which should be sufficient for most use cases. For more security, users should wait |
| 15 | + /// for finality instead (bytes2(0)). |
| 16 | + uint256 public constant BLOCK_DEPTH_BITS = 10; |
| 17 | + /// @notice The maximum block depth that can be encoded in the finality params: 1023. |
| 18 | + uint16 public constant MAX_BLOCK_DEPTH = uint16((1 << BLOCK_DEPTH_BITS) - 1); |
| 19 | + /// @notice The block depth mask to extract the block depth from the finality params. |
| 20 | + bytes2 public constant BLOCK_DEPTH_MASK = bytes2(MAX_BLOCK_DEPTH); |
| 21 | + |
| 22 | + /// @notice The finality flag for waiting for finality is 0, this is the safest option. Any block depth that's deeper |
| 23 | + /// than finality will fall back to finality, meaning a very deep block depth will not be more secure than finality. |
| 24 | + bytes2 public constant WAIT_FOR_FINALITY_FLAG = bytes2(0); |
| 25 | + /// @notice Signals to wait for the `safe` tag. |
| 26 | + bytes2 public constant WAIT_FOR_SAFE_FLAG = bytes2(uint16(1 << BLOCK_DEPTH_BITS)); |
| 27 | + |
| 28 | + /// @notice Helper to encode block depth into the finality params. Will revert if the block depth is greater than the |
| 29 | + /// maximum block depth. |
| 30 | + /// @param blockDepth The block depth to encode into the finality params. |
| 31 | + /// @return The encoded finality params with the block depth. |
| 32 | + function _encodeBlockDepth( |
| 33 | + uint16 blockDepth |
| 34 | + ) internal pure returns (bytes2) { |
| 35 | + if (blockDepth > MAX_BLOCK_DEPTH) { |
| 36 | + revert InvalidBlockDepth(blockDepth, MAX_BLOCK_DEPTH); |
| 37 | + } |
| 38 | + return bytes2(blockDepth); |
| 39 | + } |
| 40 | + |
| 41 | + /// @notice Helper to encode the `safe` tag plus a block depth into the finality params. |
| 42 | + /// NOTE: this format is only allowed for allowed finality, not requested finality, as requested finality can only |
| 43 | + /// contain a single flag or block depth, but allowed finality can contain multiple. |
| 44 | + /// @param blockDepth The block depth to encode into the finality params. |
| 45 | + /// @return The encoded finality params with the `safe` tag and block depth. |
| 46 | + function _encodeBlockDepthAndSafeFlag( |
| 47 | + uint16 blockDepth |
| 48 | + ) internal pure returns (bytes2) { |
| 49 | + return _encodeBlockDepth(blockDepth) | WAIT_FOR_SAFE_FLAG; |
| 50 | + } |
| 51 | + |
| 52 | + /// @notice Validates requested finality: either `bytes2(0)`, exactly `WAIT_FOR_SAFE_FLAG`, or a pure block depth |
| 53 | + /// (no flag bits, depth in `1..MAX_BLOCK_DEPTH`). Never a flag combined with a non-zero depth. |
| 54 | + /// @param encodedFinality The encoded finality params to validate. |
| 55 | + function _validateRequestedFinality( |
| 56 | + bytes2 encodedFinality |
| 57 | + ) internal pure { |
| 58 | + // Waiting for finality is always valid. |
| 59 | + if (encodedFinality == WAIT_FOR_FINALITY_FLAG) { |
| 60 | + return; |
| 61 | + } |
| 62 | + uint16 finality = uint16(encodedFinality); |
| 63 | + bool hasBlockDepth = (finality & MAX_BLOCK_DEPTH) != 0; |
| 64 | + uint256 activeModes = hasBlockDepth ? 1 : 0; // If it has depth, it counts as one active mode. |
| 65 | + |
| 66 | + uint16 flags = finality >> BLOCK_DEPTH_BITS; |
| 67 | + if (flags != 0) { |
| 68 | + for (uint256 i = 0; i < 6; ++i) { |
| 69 | + if ((flags & (1 << i)) != 0) { |
| 70 | + activeModes += 1; |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + // There must be exactly one active mode: either a block depth or a single flag. Selecting multiple modes is only |
| 75 | + // allowed for `allowedFinality` set by Pools, CCVs, etc., but not for `requestedFinality` set by senders. |
| 76 | + if (activeModes != 1) { |
| 77 | + revert InvalidRequestedFinality(encodedFinality); |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + /// @notice Ensures `requestedFinality` is permitted by `allowedFinality`. When matching on flags, the request must not |
| 82 | + /// carry a block depth (lower bits zero aside from the all-zero finality case, which is handled earlier). |
| 83 | + /// @param requestedFinality The requested finality params to check. This value must already be validated by |
| 84 | + /// `_validateRequestedFinality` to ensure it is well-formed. |
| 85 | + /// @param allowedFinality The allowed finality params to check against. |
| 86 | + function _ensureRequestedFinalityAllowed( |
| 87 | + bytes2 requestedFinality, |
| 88 | + bytes2 allowedFinality |
| 89 | + ) internal pure { |
| 90 | + // Finality is always allowed. |
| 91 | + if (requestedFinality == bytes2(0)) { |
| 92 | + return; |
| 93 | + } |
| 94 | + // If any of the flags match, the request is allowed only when it has no depth field (flag-only request). |
| 95 | + if (requestedFinality >> BLOCK_DEPTH_BITS & allowedFinality >> BLOCK_DEPTH_BITS != 0) { |
| 96 | + if (uint16(requestedFinality & BLOCK_DEPTH_MASK) != 0) { |
| 97 | + revert InvalidRequestedFinality(requestedFinality); |
| 98 | + } |
| 99 | + return; |
| 100 | + } |
| 101 | + // Otherwise, it must be block-depth based. |
| 102 | + uint16 requestedBlockDepth = uint16(requestedFinality & BLOCK_DEPTH_MASK); |
| 103 | + uint16 allowedBlockDepth = uint16(allowedFinality & BLOCK_DEPTH_MASK); |
| 104 | + if (allowedBlockDepth == 0 || requestedBlockDepth > allowedBlockDepth) { |
| 105 | + revert InvalidBlockDepth(requestedBlockDepth, allowedBlockDepth); |
| 106 | + } |
| 107 | + } |
| 108 | +} |
0 commit comments