Skip to content

Commit d572faa

Browse files
committed
Create FinalityCodec.sol
1 parent 5545293 commit d572faa

10 files changed

Lines changed: 339 additions & 2 deletions

chains/evm/GNUmakefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
.PHONY: test
2+
test: ## Run the full Foundry test suite (always uses the `ccip` profile).
3+
FOUNDRY_PROFILE=ccip forge test
4+
15
# Creates a gas snapshot
26
# note `make snapshot` skips fuzz/fork/reverting tests.
37
.PHONY: snapshot
48
snapshot: ## Make a snapshot for a specific product.
5-
export FOUNDRY_PROFILE=ccip && forge snapshot --nmt "test?(Fuzz|Fork|.*_RevertWhen)_.*"
9+
FOUNDRY_PROFILE=ccip forge snapshot --nmt "test?(Fuzz|Fork|.*_RevertWhen)_.*"
610

711
.PHONY: snapshot-diff
812
snapshot-diff: ## Make a snapshot for a specific product.
9-
export FOUNDRY_PROFILE=ccip && forge snapshot --nmt "test?(Fuzz|Fork|.*_RevertWhen)_.*" --diff
13+
FOUNDRY_PROFILE=ccip forge snapshot --nmt "test?(Fuzz|Fork|.*_RevertWhen)_.*" --diff
1014

1115
.PHONY: foundry
1216
foundry: ## Install foundry.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.24;
3+
4+
import {FinalityCodec} from "../../libraries/FinalityCodec.sol";
5+
6+
/// @notice Exposes `FinalityCodec` internal functions for unit tests.
7+
contract FinalityCodecHelper {
8+
function encodeBlockDepth(
9+
uint16 blockDepth
10+
) external pure returns (bytes2) {
11+
return FinalityCodec._encodeBlockDepth(blockDepth);
12+
}
13+
14+
function encodeBlockDepthAndSafeFlag(
15+
uint16 blockDepth
16+
) external pure returns (bytes2) {
17+
return FinalityCodec._encodeBlockDepthAndSafeFlag(blockDepth);
18+
}
19+
20+
function validateRequestedFinality(
21+
bytes2 encodedFinality
22+
) external pure {
23+
FinalityCodec._validateRequestedFinality(encodedFinality);
24+
}
25+
26+
function ensureRequestedFinalityAllowed(
27+
bytes2 requestedFinality,
28+
bytes2 allowedFinality
29+
) external pure {
30+
FinalityCodec._ensureRequestedFinalityAllowed(requestedFinality, allowedFinality);
31+
}
32+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.24;
3+
4+
import {FinalityCodec} from "../../../libraries/FinalityCodec.sol";
5+
import {FinalityCodecSetup} from "./FinalityCodecSetup.t.sol";
6+
7+
contract FinalityCodec__encodeBlockDepth is FinalityCodecSetup {
8+
function test__encodeBlockDepth_ZeroAndMax() public view {
9+
assertEq(bytes2(uint16(0)), s_helper.encodeBlockDepth(0));
10+
assertEq(bytes2(uint16(1023)), s_helper.encodeBlockDepth(1023));
11+
}
12+
13+
function test__encodeBlockDepth_ArbitraryDepth() public view {
14+
assertEq(bytes2(uint16(100)), s_helper.encodeBlockDepth(100));
15+
}
16+
17+
// Reverts
18+
19+
function test__encodeBlockDepth_RevertWhen_InvalidBlockDepth_DepthExceedsMax() public {
20+
vm.expectRevert(
21+
abi.encodeWithSelector(FinalityCodec.InvalidBlockDepth.selector, uint16(1024), FinalityCodec.MAX_BLOCK_DEPTH)
22+
);
23+
s_helper.encodeBlockDepth(1024);
24+
}
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.24;
3+
4+
import {FinalityCodec} from "../../../libraries/FinalityCodec.sol";
5+
import {FinalityCodecSetup} from "./FinalityCodecSetup.t.sol";
6+
7+
contract FinalityCodec__encodeBlockDepthAndSafeFlag is FinalityCodecSetup {
8+
function test__encodeBlockDepthAndSafeFlag_CombinesFlagAndDepth() public view {
9+
assertEq(
10+
FinalityCodec.WAIT_FOR_SAFE_FLAG,
11+
s_helper.encodeBlockDepthAndSafeFlag(0),
12+
"depth 0 leaves only the safe flag in the lower bits as 0"
13+
);
14+
bytes2 expected = bytes2(uint16(uint16(FinalityCodec.WAIT_FOR_SAFE_FLAG) | 42));
15+
assertEq(expected, s_helper.encodeBlockDepthAndSafeFlag(42));
16+
}
17+
18+
// Reverts
19+
20+
function test__encodeBlockDepthAndSafeFlag_RevertWhen_InvalidBlockDepth_DepthExceedsMax() public {
21+
vm.expectRevert(
22+
abi.encodeWithSelector(FinalityCodec.InvalidBlockDepth.selector, uint16(1024), FinalityCodec.MAX_BLOCK_DEPTH)
23+
);
24+
s_helper.encodeBlockDepthAndSafeFlag(1024);
25+
}
26+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.24;
3+
4+
import {FinalityCodec} from "../../../libraries/FinalityCodec.sol";
5+
import {FinalityCodecSetup} from "./FinalityCodecSetup.t.sol";
6+
7+
contract FinalityCodec__ensureRequestedFinalityAllowed is FinalityCodecSetup {
8+
function test__ensureRequestedFinalityAllowed_FinalityAlwaysAllowed() public view {
9+
s_helper.ensureRequestedFinalityAllowed(bytes2(0), bytes2(0));
10+
s_helper.ensureRequestedFinalityAllowed(bytes2(0), bytes2(uint16(1)));
11+
s_helper.ensureRequestedFinalityAllowed(bytes2(0), FinalityCodec.WAIT_FOR_SAFE_FLAG);
12+
}
13+
14+
function test__ensureRequestedFinalityAllowed_AllowedWhen_UpperFlagBitsOverlap() public view {
15+
bytes2 requested = FinalityCodec.WAIT_FOR_SAFE_FLAG;
16+
bytes2 allowed = bytes2(uint16(uint16(FinalityCodec.WAIT_FOR_SAFE_FLAG) | 500));
17+
s_helper.ensureRequestedFinalityAllowed(requested, allowed);
18+
}
19+
20+
function test__ensureRequestedFinalityAllowed_AllowedWhen_BlockDepthWithinAllowance() public view {
21+
s_helper.ensureRequestedFinalityAllowed(bytes2(uint16(50)), bytes2(uint16(100)));
22+
s_helper.ensureRequestedFinalityAllowed(bytes2(uint16(100)), bytes2(uint16(100)));
23+
}
24+
25+
function test__ensureRequestedFinalityAllowed_AllowedWhen_RequestedSafeAndAllowedIsDepthOnly() public view {
26+
s_helper.ensureRequestedFinalityAllowed(FinalityCodec.WAIT_FOR_SAFE_FLAG, bytes2(uint16(200)));
27+
}
28+
29+
/// forge-config: default.fuzz.runs = 1024
30+
/// forge-config: ccip.fuzz.runs = 1024
31+
function testFuzz__ensureRequestedFinalityAllowed_FinalityAlwaysAllowed(
32+
bytes2 allowed
33+
) public view {
34+
s_helper.ensureRequestedFinalityAllowed(bytes2(0), allowed);
35+
}
36+
37+
/// forge-config: default.fuzz.runs = 1024
38+
/// forge-config: ccip.fuzz.runs = 1024
39+
function testFuzz__ensureRequestedFinalityAllowed_BlockDepthAllowedWhen_LessOrEqual(
40+
uint16 allowedDepth,
41+
uint16 requestedDepth
42+
) public view {
43+
allowedDepth = uint16(bound(uint256(allowedDepth), 0, uint256(FinalityCodec.MAX_BLOCK_DEPTH)));
44+
requestedDepth = uint16(bound(uint256(requestedDepth), 0, uint256(allowedDepth)));
45+
s_helper.ensureRequestedFinalityAllowed(bytes2(requestedDepth), bytes2(allowedDepth));
46+
}
47+
48+
// Reverts
49+
50+
function test__ensureRequestedFinalityAllowed_RevertWhen_InvalidBlockDepth_RequestedDepthExceedsAllowed() public {
51+
bytes2 requested = bytes2(uint16(101));
52+
bytes2 allowed = bytes2(uint16(100));
53+
vm.expectRevert(abi.encodeWithSelector(FinalityCodec.InvalidBlockDepth.selector, uint16(101), uint16(100)));
54+
s_helper.ensureRequestedFinalityAllowed(requested, allowed);
55+
}
56+
57+
function test__ensureRequestedFinalityAllowed_RevertWhen_InvalidBlockDepth_NoMatchingFlagAndRequestedDepthExceedsAllowed()
58+
public
59+
{
60+
bytes2 requested = bytes2(uint16(1));
61+
bytes2 allowed = FinalityCodec.WAIT_FOR_SAFE_FLAG;
62+
vm.expectRevert(abi.encodeWithSelector(FinalityCodec.InvalidBlockDepth.selector, uint16(1), uint16(0)));
63+
s_helper.ensureRequestedFinalityAllowed(requested, allowed);
64+
}
65+
66+
/// @dev Documents `_ensureRequestedFinalityAllowed` when flag bits overlap but lower bits are non-zero (ill-formed
67+
/// for a validated request; callers should run `_validateRequestedFinality` first).
68+
function test__ensureRequestedFinalityAllowed_RevertWhen_InvalidRequestedFinality_FlagOverlapWithNonZeroDepth()
69+
public
70+
{
71+
bytes2 requested = bytes2(uint16(uint16(FinalityCodec.WAIT_FOR_SAFE_FLAG) | 7));
72+
bytes2 allowed = bytes2(uint16(uint16(FinalityCodec.WAIT_FOR_SAFE_FLAG) | 500));
73+
vm.expectRevert(abi.encodeWithSelector(FinalityCodec.InvalidRequestedFinality.selector, requested));
74+
s_helper.ensureRequestedFinalityAllowed(requested, allowed);
75+
}
76+
77+
/// forge-config: default.fuzz.runs = 1024
78+
/// forge-config: ccip.fuzz.runs = 1024
79+
function testFuzz__ensureRequestedFinalityAllowed_RevertWhen_InvalidBlockDepth_RequestedDepthGreaterThanAllowed(
80+
uint16 allowedDepth,
81+
uint16 requestedDepth
82+
) public {
83+
allowedDepth = uint16(bound(uint256(allowedDepth), 0, uint256(FinalityCodec.MAX_BLOCK_DEPTH - 1)));
84+
requestedDepth =
85+
uint16(bound(uint256(requestedDepth), uint256(allowedDepth) + 1, uint256(FinalityCodec.MAX_BLOCK_DEPTH)));
86+
vm.expectRevert(abi.encodeWithSelector(FinalityCodec.InvalidBlockDepth.selector, requestedDepth, allowedDepth));
87+
s_helper.ensureRequestedFinalityAllowed(bytes2(requestedDepth), bytes2(allowedDepth));
88+
}
89+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.24;
3+
4+
import {FinalityCodec} from "../../../libraries/FinalityCodec.sol";
5+
import {FinalityCodecSetup} from "./FinalityCodecSetup.t.sol";
6+
7+
contract FinalityCodec__validateRequestedFinality is FinalityCodecSetup {
8+
function test__validateRequestedFinality_WaitForFinality() public view {
9+
s_helper.validateRequestedFinality(FinalityCodec.WAIT_FOR_FINALITY_FLAG);
10+
}
11+
12+
function test__validateRequestedFinality_WaitForSafe() public view {
13+
s_helper.validateRequestedFinality(FinalityCodec.WAIT_FOR_SAFE_FLAG);
14+
}
15+
16+
function test__validateRequestedFinality_PureBlockDepth_Boundaries() public view {
17+
s_helper.validateRequestedFinality(bytes2(uint16(1)));
18+
s_helper.validateRequestedFinality(bytes2(FinalityCodec.MAX_BLOCK_DEPTH));
19+
}
20+
21+
function test__validateRequestedFinality_PureBlockDepth_MidRange() public view {
22+
s_helper.validateRequestedFinality(bytes2(uint16(500)));
23+
}
24+
25+
// Reverts
26+
27+
function test__validateRequestedFinality_RevertWhen_InvalidRequestedFinality_FlagWithNonZeroDepth() public {
28+
bytes2 invalid = bytes2(uint16(uint16(FinalityCodec.WAIT_FOR_SAFE_FLAG) | 1));
29+
vm.expectRevert(abi.encodeWithSelector(FinalityCodec.InvalidRequestedFinality.selector, invalid));
30+
s_helper.validateRequestedFinality(invalid);
31+
}
32+
33+
function test__validateRequestedFinality_RevertWhen_InvalidRequestedFinality_MultipleFlagBits() public {
34+
bytes2 invalid = bytes2(uint16((1 << 10) | (1 << 11)));
35+
vm.expectRevert(abi.encodeWithSelector(FinalityCodec.InvalidRequestedFinality.selector, invalid));
36+
s_helper.validateRequestedFinality(invalid);
37+
}
38+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.24;
3+
4+
import {FinalityCodecHelper} from "../../helpers/FinalityCodecHelper.sol";
5+
import {Test} from "forge-std/Test.sol";
6+
7+
contract FinalityCodecSetup is Test {
8+
FinalityCodecHelper internal s_helper;
9+
10+
function setUp() public virtual {
11+
s_helper = new FinalityCodecHelper();
12+
}
13+
}

chains/evm/foundry.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# Run Forge tests, coverage, and snapshots with `FOUNDRY_PROFILE=ccip` (see `GNUmakefile` and `package.json`).
12
[profile.default]
23
solc_version = '0.8.26'
34
evm_version = 'paris'

chains/evm/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"license": "BUSL-1.1",
88
"private": false,
99
"scripts": {
10+
"test": "FOUNDRY_PROFILE=ccip forge test",
1011
"publish-beta": "pnpm publish --tag beta",
1112
"publish-prod": "pnpm publish --tag latest",
1213
"compile": "./scripts/compile_all",

0 commit comments

Comments
 (0)