Skip to content

Commit f1a69f1

Browse files
ernestognwAmxx
andauthored
Add Halmos support for formal verification (#5034)
Co-authored-by: Hadrien Croubois <[email protected]>
1 parent 9de916d commit f1a69f1

File tree

17 files changed

+197
-38
lines changed

17 files changed

+197
-38
lines changed

.github/actions/gas-compare/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: Compare gas costs
2+
description: Compare gas costs between branches
23
inputs:
34
token:
45
description: github token

.github/actions/setup/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: Setup
2+
description: Common environment setup
23

34
runs:
45
using: composite

.github/actions/storage-layout/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: Compare storage layouts
2+
description: Compare storage layouts between branches
23
inputs:
34
token:
45
description: github token

.github/workflows/formal-verification.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ jobs:
4848
with:
4949
python-version: ${{ env.PIP_VERSION }}
5050
cache: 'pip'
51+
cache-dependency-path: 'fv-requirements.txt'
5152
- name: Install python packages
52-
run: pip install -r requirements.txt
53+
run: pip install -r fv-requirements.txt
5354
- name: Install java
5455
uses: actions/setup-java@v3
5556
with:
@@ -66,3 +67,20 @@ jobs:
6667
node certora/run.js ${{ steps.arguments.outputs.result }} >> "$GITHUB_STEP_SUMMARY"
6768
env:
6869
CERTORAKEY: ${{ secrets.CERTORAKEY }}
70+
71+
halmos:
72+
runs-on: ubuntu-latest
73+
steps:
74+
- uses: actions/checkout@v4
75+
- name: Set up environment
76+
uses: ./.github/actions/setup
77+
- name: Install python
78+
uses: actions/setup-python@v5
79+
with:
80+
python-version: ${{ env.PIP_VERSION }}
81+
cache: 'pip'
82+
cache-dependency-path: 'fv-requirements.txt'
83+
- name: Install python packages
84+
run: pip install -r fv-requirements.txt
85+
- name: Run Halmos
86+
run: halmos --match-test '^symbolic|^testSymbolic' -vv

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@
55
[submodule "lib/erc4626-tests"]
66
path = lib/erc4626-tests
77
url = https://github.com/a16z/erc4626-tests.git
8+
[submodule "lib/halmos-cheatcodes"]
9+
path = lib/halmos-cheatcodes
10+
url = https://github.com/a16z/halmos-cheatcodes

fv-requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
certora-cli==4.13.1
2+
# File uses a custom name (fv-requirements.txt) so that it isn't picked by Netlify's build
3+
# whose latest Python version is 0.3.8, incompatible with most recent versions of Halmos
4+
halmos==0.1.12

lib/halmos-cheatcodes

Submodule halmos-cheatcodes added at c0d8655

requirements.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

scripts/generate/templates/SlotDerivation.t.js

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,26 @@ const header = `\
66
pragma solidity ^0.8.20;
77
88
import {Test} from "forge-std/Test.sol";
9-
9+
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
1010
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
1111
`;
1212

1313
const array = `\
1414
bytes[] private _array;
1515
16+
function symbolicDeriveArray(uint256 length, uint256 offset) public {
17+
vm.assume(length > 0);
18+
vm.assume(offset < length);
19+
_assertDeriveArray(length, offset);
20+
}
21+
1622
function testDeriveArray(uint256 length, uint256 offset) public {
1723
length = bound(length, 1, type(uint256).max);
1824
offset = bound(offset, 0, length - 1);
25+
_assertDeriveArray(length, offset);
26+
}
1927
28+
function _assertDeriveArray(uint256 length, uint256 offset) public {
2029
bytes32 baseSlot;
2130
assembly {
2231
baseSlot := _array.slot
@@ -33,10 +42,10 @@ function testDeriveArray(uint256 length, uint256 offset) public {
3342
}
3443
`;
3544

36-
const mapping = ({ type, name, isValueType }) => `\
45+
const mapping = ({ type, name }) => `\
3746
mapping(${type} => bytes) private _${type}Mapping;
3847
39-
function testDeriveMapping${name}(${type} ${isValueType ? '' : 'memory'} key) public {
48+
function testSymbolicDeriveMapping${name}(${type} key) public {
4049
bytes32 baseSlot;
4150
assembly {
4251
baseSlot := _${type}Mapping.slot
@@ -52,10 +61,37 @@ function testDeriveMapping${name}(${type} ${isValueType ? '' : 'memory'} key) pu
5261
}
5362
`;
5463

64+
const boundedMapping = ({ type, name }) => `\
65+
mapping(${type} => bytes) private _${type}Mapping;
66+
67+
function testDeriveMapping${name}(${type} memory key) public {
68+
_assertDeriveMapping${name}(key);
69+
}
70+
71+
function symbolicDeriveMapping${name}() public {
72+
_assertDeriveMapping${name}(svm.create${name}(256, "DeriveMapping${name}Input"));
73+
}
74+
75+
function _assertDeriveMapping${name}(${type} memory key) internal {
76+
bytes32 baseSlot;
77+
assembly {
78+
baseSlot := _${type}Mapping.slot
79+
}
80+
81+
bytes storage derived = _${type}Mapping[key];
82+
bytes32 derivedSlot;
83+
assembly {
84+
derivedSlot := derived.slot
85+
}
86+
87+
assertEq(baseSlot.deriveMapping(key), derivedSlot);
88+
}
89+
`;
90+
5591
// GENERATE
5692
module.exports = format(
5793
header.trimEnd(),
58-
'contract SlotDerivationTest is Test {',
94+
'contract SlotDerivationTest is Test, SymTest {',
5995
'using SlotDerivation for bytes32;',
6096
'',
6197
array,
@@ -68,6 +104,6 @@ module.exports = format(
68104
isValueType: type.isValueType,
69105
})),
70106
),
71-
).map(type => mapping(type)),
107+
).map(type => (type.isValueType ? mapping(type) : boundedMapping(type))),
72108
'}',
73109
);

test/proxy/Clones.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Test} from "forge-std/Test.sol";
66
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
77

88
contract ClonesTest is Test {
9-
function testPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public {
9+
function testSymbolicPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public {
1010
address predicted = Clones.predictDeterministicAddress(implementation, salt);
1111
bytes32 spillage;
1212
/// @solidity memory-safe-assembly

test/utils/Arrays.t.sol

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,27 @@
33
pragma solidity ^0.8.20;
44

55
import {Test} from "forge-std/Test.sol";
6+
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
67
import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol";
78

8-
contract ArraysTest is Test {
9+
contract ArraysTest is Test, SymTest {
910
function testSort(uint256[] memory values) public {
1011
Arrays.sort(values);
12+
_assertSort(values);
13+
}
14+
15+
function symbolicSort() public {
16+
uint256[] memory values = new uint256[](3);
17+
for (uint256 i = 0; i < 3; i++) {
18+
values[i] = svm.createUint256("arrayElement");
19+
}
20+
Arrays.sort(values);
21+
_assertSort(values);
22+
}
23+
24+
/// Asserts
25+
26+
function _assertSort(uint256[] memory values) internal {
1127
for (uint256 i = 1; i < values.length; ++i) {
1228
assertLe(values[i - 1], values[i]);
1329
}

test/utils/Create2.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Test} from "forge-std/Test.sol";
66
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
77

88
contract Create2Test is Test {
9-
function testComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public {
9+
function testSymbolicComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public {
1010
address predicted = Create2.computeAddress(salt, bytecodeHash, deployer);
1111
bytes32 spillage;
1212
/// @solidity memory-safe-assembly

test/utils/Packing.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ contract PackingTest is Test {
99
using Packing for *;
1010

1111
// Pack a pair of arbitrary uint128, and check that split recovers the correct values
12-
function testUint128x2(uint128 first, uint128 second) external {
12+
function testSymbolicUint128x2(uint128 first, uint128 second) external {
1313
Packing.Uint128x2 packed = Packing.pack(first, second);
1414
assertEq(packed.first(), first);
1515
assertEq(packed.second(), second);
@@ -20,7 +20,7 @@ contract PackingTest is Test {
2020
}
2121

2222
// split an arbitrary bytes32 into a pair of uint128, and check that repack matches the input
23-
function testUint128x2(bytes32 input) external {
23+
function testSymbolicUint128x2(bytes32 input) external {
2424
(uint128 first, uint128 second) = input.asUint128x2().split();
2525
assertEq(Packing.pack(first, second).asBytes32(), input);
2626
}

test/utils/ShortStrings.t.sol

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,102 @@
33
pragma solidity ^0.8.20;
44

55
import {Test} from "forge-std/Test.sol";
6+
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
67

78
import {ShortStrings, ShortString} from "@openzeppelin/contracts/utils/ShortStrings.sol";
89

9-
contract ShortStringsTest is Test {
10+
contract ShortStringsTest is Test, SymTest {
1011
string _fallback;
1112

1213
function testRoundtripShort(string memory input) external {
1314
vm.assume(_isShort(input));
15+
_assertRoundtripShort(input);
16+
}
17+
18+
function symbolicRoundtripShort() external {
19+
string memory input = svm.createString(31, "RoundtripShortInput");
20+
_assertRoundtripShort(input);
21+
}
22+
23+
function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external {
24+
_assertRoundtripWithFallback(input, fallbackInitial);
25+
}
26+
27+
function symbolicRoundtripWithFallbackLong() external {
28+
string memory input = svm.createString(256, "RoundtripWithFallbackInput");
29+
string memory fallbackInitial = svm.createString(256, "RoundtripWithFallbackFallbackInitial");
30+
_assertRoundtripWithFallback(input, fallbackInitial);
31+
}
32+
33+
function symbolicRoundtripWithFallbackShort() external {
34+
string memory input = svm.createString(31, "RoundtripWithFallbackInput");
35+
string memory fallbackInitial = svm.createString(31, "RoundtripWithFallbackFallbackInitial");
36+
_assertRoundtripWithFallback(input, fallbackInitial);
37+
}
38+
39+
function testRevertLong(string memory input) external {
40+
vm.assume(!_isShort(input));
41+
_assertRevertLong(input);
42+
}
43+
44+
function testLengthShort(string memory input) external {
45+
vm.assume(_isShort(input));
46+
_assertLengthShort(input);
47+
}
48+
49+
function symbolicLengthShort() external {
50+
string memory input = svm.createString(31, "LengthShortInput");
51+
_assertLengthShort(input);
52+
}
53+
54+
function testLengthWithFallback(string memory input, string memory fallbackInitial) external {
55+
_fallback = fallbackInitial;
56+
_assertLengthWithFallback(input);
57+
}
58+
59+
function symbolicLengthWithFallback() external {
60+
uint256 length = 256;
61+
string memory input = svm.createString(length, "LengthWithFallbackInput");
62+
string memory fallbackInitial = svm.createString(length, "LengthWithFallbackFallbackInitial");
63+
_fallback = fallbackInitial;
64+
_assertLengthWithFallback(input);
65+
}
66+
67+
/// Assertions
68+
69+
function _assertRoundtripShort(string memory input) internal {
1470
ShortString short = ShortStrings.toShortString(input);
1571
string memory output = ShortStrings.toString(short);
1672
assertEq(input, output);
1773
}
1874

19-
function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external {
75+
function _assertRoundtripWithFallback(string memory input, string memory fallbackInitial) internal {
2076
_fallback = fallbackInitial; // Make sure that the initial value has no effect
2177
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
2278
string memory output = ShortStrings.toStringWithFallback(short, _fallback);
2379
assertEq(input, output);
2480
}
2581

26-
function testRevertLong(string memory input) external {
27-
vm.assume(!_isShort(input));
82+
function _assertRevertLong(string memory input) internal {
2883
vm.expectRevert(abi.encodeWithSelector(ShortStrings.StringTooLong.selector, input));
2984
this.toShortString(input);
3085
}
3186

32-
function testLengthShort(string memory input) external {
33-
vm.assume(_isShort(input));
34-
uint256 inputLength = bytes(input).length;
87+
function _assertLengthShort(string memory input) internal {
3588
ShortString short = ShortStrings.toShortString(input);
3689
uint256 shortLength = ShortStrings.byteLength(short);
90+
uint256 inputLength = bytes(input).length;
3791
assertEq(inputLength, shortLength);
3892
}
3993

40-
function testLengthWithFallback(string memory input, string memory fallbackInitial) external {
41-
_fallback = fallbackInitial;
94+
function _assertLengthWithFallback(string memory input) internal {
4295
uint256 inputLength = bytes(input).length;
4396
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
4497
uint256 shortLength = ShortStrings.byteLengthWithFallback(short, _fallback);
4598
assertEq(inputLength, shortLength);
4699
}
47100

101+
/// Helpers
48102
function toShortString(string memory input) external pure returns (ShortString) {
49103
return ShortStrings.toShortString(input);
50104
}

0 commit comments

Comments
 (0)