Skip to content

Commit

Permalink
Add Halmos support for formal verification (OpenZeppelin#5034)
Browse files Browse the repository at this point in the history
Co-authored-by: Hadrien Croubois <[email protected]>
  • Loading branch information
ernestognw and Amxx authored May 23, 2024
1 parent 9de916d commit f1a69f1
Show file tree
Hide file tree
Showing 17 changed files with 197 additions and 38 deletions.
1 change: 1 addition & 0 deletions .github/actions/gas-compare/action.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: Compare gas costs
description: Compare gas costs between branches
inputs:
token:
description: github token
Expand Down
1 change: 1 addition & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: Setup
description: Common environment setup

runs:
using: composite
Expand Down
1 change: 1 addition & 0 deletions .github/actions/storage-layout/action.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: Compare storage layouts
description: Compare storage layouts between branches
inputs:
token:
description: github token
Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/formal-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ jobs:
with:
python-version: ${{ env.PIP_VERSION }}
cache: 'pip'
cache-dependency-path: 'fv-requirements.txt'
- name: Install python packages
run: pip install -r requirements.txt
run: pip install -r fv-requirements.txt
- name: Install java
uses: actions/setup-java@v3
with:
Expand All @@ -66,3 +67,20 @@ jobs:
node certora/run.js ${{ steps.arguments.outputs.result }} >> "$GITHUB_STEP_SUMMARY"
env:
CERTORAKEY: ${{ secrets.CERTORAKEY }}

halmos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Install python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PIP_VERSION }}
cache: 'pip'
cache-dependency-path: 'fv-requirements.txt'
- name: Install python packages
run: pip install -r fv-requirements.txt
- name: Run Halmos
run: halmos --match-test '^symbolic|^testSymbolic' -vv
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
[submodule "lib/erc4626-tests"]
path = lib/erc4626-tests
url = https://github.com/a16z/erc4626-tests.git
[submodule "lib/halmos-cheatcodes"]
path = lib/halmos-cheatcodes
url = https://github.com/a16z/halmos-cheatcodes
4 changes: 4 additions & 0 deletions fv-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
certora-cli==4.13.1
# File uses a custom name (fv-requirements.txt) so that it isn't picked by Netlify's build
# whose latest Python version is 0.3.8, incompatible with most recent versions of Halmos
halmos==0.1.12
1 change: 1 addition & 0 deletions lib/halmos-cheatcodes
Submodule halmos-cheatcodes added at c0d865
1 change: 0 additions & 1 deletion requirements.txt

This file was deleted.

46 changes: 41 additions & 5 deletions scripts/generate/templates/SlotDerivation.t.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,26 @@ const header = `\
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
`;

const array = `\
bytes[] private _array;
function symbolicDeriveArray(uint256 length, uint256 offset) public {
vm.assume(length > 0);
vm.assume(offset < length);
_assertDeriveArray(length, offset);
}
function testDeriveArray(uint256 length, uint256 offset) public {
length = bound(length, 1, type(uint256).max);
offset = bound(offset, 0, length - 1);
_assertDeriveArray(length, offset);
}
function _assertDeriveArray(uint256 length, uint256 offset) public {
bytes32 baseSlot;
assembly {
baseSlot := _array.slot
Expand All @@ -33,10 +42,10 @@ function testDeriveArray(uint256 length, uint256 offset) public {
}
`;

const mapping = ({ type, name, isValueType }) => `\
const mapping = ({ type, name }) => `\
mapping(${type} => bytes) private _${type}Mapping;
function testDeriveMapping${name}(${type} ${isValueType ? '' : 'memory'} key) public {
function testSymbolicDeriveMapping${name}(${type} key) public {
bytes32 baseSlot;
assembly {
baseSlot := _${type}Mapping.slot
Expand All @@ -52,10 +61,37 @@ function testDeriveMapping${name}(${type} ${isValueType ? '' : 'memory'} key) pu
}
`;

const boundedMapping = ({ type, name }) => `\
mapping(${type} => bytes) private _${type}Mapping;
function testDeriveMapping${name}(${type} memory key) public {
_assertDeriveMapping${name}(key);
}
function symbolicDeriveMapping${name}() public {
_assertDeriveMapping${name}(svm.create${name}(256, "DeriveMapping${name}Input"));
}
function _assertDeriveMapping${name}(${type} memory key) internal {
bytes32 baseSlot;
assembly {
baseSlot := _${type}Mapping.slot
}
bytes storage derived = _${type}Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
`;

// GENERATE
module.exports = format(
header.trimEnd(),
'contract SlotDerivationTest is Test {',
'contract SlotDerivationTest is Test, SymTest {',
'using SlotDerivation for bytes32;',
'',
array,
Expand All @@ -68,6 +104,6 @@ module.exports = format(
isValueType: type.isValueType,
})),
),
).map(type => mapping(type)),
).map(type => (type.isValueType ? mapping(type) : boundedMapping(type))),
'}',
);
2 changes: 1 addition & 1 deletion test/proxy/Clones.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {Test} from "forge-std/Test.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";

contract ClonesTest is Test {
function testPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public {
function testSymbolicPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public {
address predicted = Clones.predictDeterministicAddress(implementation, salt);
bytes32 spillage;
/// @solidity memory-safe-assembly
Expand Down
18 changes: 17 additions & 1 deletion test/utils/Arrays.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,27 @@
pragma solidity ^0.8.20;

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

contract ArraysTest is Test {
contract ArraysTest is Test, SymTest {
function testSort(uint256[] memory values) public {
Arrays.sort(values);
_assertSort(values);
}

function symbolicSort() public {
uint256[] memory values = new uint256[](3);
for (uint256 i = 0; i < 3; i++) {
values[i] = svm.createUint256("arrayElement");
}
Arrays.sort(values);
_assertSort(values);
}

/// Asserts

function _assertSort(uint256[] memory values) internal {
for (uint256 i = 1; i < values.length; ++i) {
assertLe(values[i - 1], values[i]);
}
Expand Down
2 changes: 1 addition & 1 deletion test/utils/Create2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {Test} from "forge-std/Test.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";

contract Create2Test is Test {
function testComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public {
function testSymbolicComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public {
address predicted = Create2.computeAddress(salt, bytecodeHash, deployer);
bytes32 spillage;
/// @solidity memory-safe-assembly
Expand Down
4 changes: 2 additions & 2 deletions test/utils/Packing.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ contract PackingTest is Test {
using Packing for *;

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

// split an arbitrary bytes32 into a pair of uint128, and check that repack matches the input
function testUint128x2(bytes32 input) external {
function testSymbolicUint128x2(bytes32 input) external {
(uint128 first, uint128 second) = input.asUint128x2().split();
assertEq(Packing.pack(first, second).asBytes32(), input);
}
Expand Down
72 changes: 63 additions & 9 deletions test/utils/ShortStrings.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,102 @@
pragma solidity ^0.8.20;

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

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

contract ShortStringsTest is Test {
contract ShortStringsTest is Test, SymTest {
string _fallback;

function testRoundtripShort(string memory input) external {
vm.assume(_isShort(input));
_assertRoundtripShort(input);
}

function symbolicRoundtripShort() external {
string memory input = svm.createString(31, "RoundtripShortInput");
_assertRoundtripShort(input);
}

function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external {
_assertRoundtripWithFallback(input, fallbackInitial);
}

function symbolicRoundtripWithFallbackLong() external {
string memory input = svm.createString(256, "RoundtripWithFallbackInput");
string memory fallbackInitial = svm.createString(256, "RoundtripWithFallbackFallbackInitial");
_assertRoundtripWithFallback(input, fallbackInitial);
}

function symbolicRoundtripWithFallbackShort() external {
string memory input = svm.createString(31, "RoundtripWithFallbackInput");
string memory fallbackInitial = svm.createString(31, "RoundtripWithFallbackFallbackInitial");
_assertRoundtripWithFallback(input, fallbackInitial);
}

function testRevertLong(string memory input) external {
vm.assume(!_isShort(input));
_assertRevertLong(input);
}

function testLengthShort(string memory input) external {
vm.assume(_isShort(input));
_assertLengthShort(input);
}

function symbolicLengthShort() external {
string memory input = svm.createString(31, "LengthShortInput");
_assertLengthShort(input);
}

function testLengthWithFallback(string memory input, string memory fallbackInitial) external {
_fallback = fallbackInitial;
_assertLengthWithFallback(input);
}

function symbolicLengthWithFallback() external {
uint256 length = 256;
string memory input = svm.createString(length, "LengthWithFallbackInput");
string memory fallbackInitial = svm.createString(length, "LengthWithFallbackFallbackInitial");
_fallback = fallbackInitial;
_assertLengthWithFallback(input);
}

/// Assertions

function _assertRoundtripShort(string memory input) internal {
ShortString short = ShortStrings.toShortString(input);
string memory output = ShortStrings.toString(short);
assertEq(input, output);
}

function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external {
function _assertRoundtripWithFallback(string memory input, string memory fallbackInitial) internal {
_fallback = fallbackInitial; // Make sure that the initial value has no effect
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
string memory output = ShortStrings.toStringWithFallback(short, _fallback);
assertEq(input, output);
}

function testRevertLong(string memory input) external {
vm.assume(!_isShort(input));
function _assertRevertLong(string memory input) internal {
vm.expectRevert(abi.encodeWithSelector(ShortStrings.StringTooLong.selector, input));
this.toShortString(input);
}

function testLengthShort(string memory input) external {
vm.assume(_isShort(input));
uint256 inputLength = bytes(input).length;
function _assertLengthShort(string memory input) internal {
ShortString short = ShortStrings.toShortString(input);
uint256 shortLength = ShortStrings.byteLength(short);
uint256 inputLength = bytes(input).length;
assertEq(inputLength, shortLength);
}

function testLengthWithFallback(string memory input, string memory fallbackInitial) external {
_fallback = fallbackInitial;
function _assertLengthWithFallback(string memory input) internal {
uint256 inputLength = bytes(input).length;
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
uint256 shortLength = ShortStrings.byteLengthWithFallback(short, _fallback);
assertEq(inputLength, shortLength);
}

/// Helpers
function toShortString(string memory input) external pure returns (ShortString) {
return ShortStrings.toShortString(input);
}
Expand Down
Loading

0 comments on commit f1a69f1

Please sign in to comment.