Skip to content

Commit 8a1dd94

Browse files
horsefactsdaejunparkvarunsrin
authored
test: IdRegistry symbolic tests (#397)
* install halmos-cheatcodes * add symtest for IdRegistry and KeyRegistry * add halmos to ci * ci: separate job for symtest using macos runner * ci: install halmos release * ci: update halmos command * test: update symtest * refactor: rename files and test job for clarity * chore: separate helpers * wip: update symbolic tests * wip: temporary mitigation for halmos bug * Revert "wip: temporary mitigation for halmos bug" This reverts commit c2b143c. * chore: update tests for the latest interface * test: add more properties for addFor and removeFor * test: remove unused variable * test: remove unnecessary setup configurations * test: update symbolic tests * test: remove key registry symtests --------- Co-authored-by: Daejun Park <[email protected]> Co-authored-by: Varun Srinivasan <[email protected]>
1 parent 5aa0d3b commit 8a1dd94

File tree

4 files changed

+239
-0
lines changed

4 files changed

+239
-0
lines changed

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,26 @@ jobs:
119119
- name: Check forge snapshots
120120
run: forge snapshot --check --match-contract Gas
121121

122+
halmos:
123+
runs-on: macos-latest
124+
steps:
125+
- uses: actions/checkout@v3
126+
with:
127+
submodules: recursive
128+
129+
- name: Install foundry
130+
uses: foundry-rs/foundry-toolchain@v1
131+
132+
- uses: actions/setup-python@v4
133+
with:
134+
python-version: "3.11"
135+
136+
- name: Install halmos
137+
run: pip install halmos
138+
139+
- name: Run halmos
140+
run: halmos --error-unknown --test-parallel --solver-parallel --solver-timeout-assertion 0
141+
122142
coverage:
123143
runs-on: ${{ vars.BUILDJET_DISABLED == 'true' && 'ubuntu-latest' || 'buildjet-2vcpu-ubuntu-2204' }}
124144
steps:

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@
1515
[submodule "lib/openzeppelin-latest"]
1616
path = lib/openzeppelin-latest
1717
url = https://github.com/OpenZeppelin/openzeppelin-contracts
18+
[submodule "lib/halmos-cheatcodes"]
19+
path = lib/halmos-cheatcodes
20+
url = https://github.com/a16z/halmos-cheatcodes

lib/halmos-cheatcodes

Submodule halmos-cheatcodes added at d988df8
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.19;
3+
4+
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
5+
import {Test} from "forge-std/Test.sol";
6+
7+
import {IdRegistry} from "../../src/IdRegistry.sol";
8+
9+
contract IdRegistrySymTest is SymTest, Test {
10+
IdRegistry idRegistry;
11+
address trustedCaller;
12+
address x;
13+
address y;
14+
15+
function setUp() public {
16+
// Setup IdRegistry
17+
idRegistry = new IdRegistry(address(this));
18+
19+
trustedCaller = address(0x1000);
20+
idRegistry.setTrustedCaller(trustedCaller);
21+
22+
// Register fids
23+
vm.prank(trustedCaller);
24+
idRegistry.trustedRegister(address(0x1001), address(0x2001));
25+
vm.prank(trustedCaller);
26+
idRegistry.trustedRegister(address(0x1002), address(0x2002));
27+
28+
assert(idRegistry.idOf(address(0x1001)) == 1);
29+
assert(idRegistry.idOf(address(0x1002)) == 2);
30+
31+
assert(idRegistry.recoveryOf(1) == address(0x2001));
32+
assert(idRegistry.recoveryOf(2) == address(0x2002));
33+
34+
// Create symbolic addresses
35+
x = svm.createAddress("x");
36+
y = svm.createAddress("y");
37+
}
38+
39+
function check_Invariants(bytes4 selector, address caller) public {
40+
_initState();
41+
vm.assume(x != y);
42+
43+
// Record pre-state
44+
uint256 oldIdX = idRegistry.idOf(x);
45+
uint256 oldIdY = idRegistry.idOf(y);
46+
uint256 oldIdCounter = idRegistry.idCounter();
47+
bool oldPaused = idRegistry.paused();
48+
49+
// Execute an arbitrary tx to IdRegistry
50+
vm.prank(caller);
51+
(bool success,) = address(idRegistry).call(_calldataFor(selector));
52+
vm.assume(success); // ignore reverting cases
53+
54+
// Record post-state
55+
uint256 newIdX = idRegistry.idOf(x);
56+
uint256 newIdY = idRegistry.idOf(y);
57+
uint256 newIdCounter = idRegistry.idCounter();
58+
59+
// Ensure that there is no recovery address associated with fid 0.
60+
assert(idRegistry.recoveryOf(0) == address(0));
61+
62+
// Ensure that idCounter never decreases.
63+
assert(newIdCounter >= oldIdCounter);
64+
65+
// If a new fid is registered, ensure that:
66+
// - IdRegistry must not be paused.
67+
// - idCounter must increase by 1.
68+
// - The new fid must not be registered for an existing fid owner.
69+
// - The existing fids must be preserved.
70+
if (newIdCounter > oldIdCounter) {
71+
assert(oldPaused == false);
72+
assert(newIdCounter - oldIdCounter == 1);
73+
assert(newIdX == oldIdX || oldIdX == 0);
74+
assert(newIdX == oldIdX || newIdY == oldIdY);
75+
}
76+
}
77+
78+
function check_Transfer(address caller, address to, address other) public {
79+
_initState();
80+
vm.assume(other != caller && other != to);
81+
82+
// Record pre-state
83+
uint256 oldIdCaller = idRegistry.idOf(caller);
84+
uint256 oldIdTo = idRegistry.idOf(to);
85+
uint256 oldIdOther = idRegistry.idOf(other);
86+
87+
// Execute transfer with symbolic arguments
88+
vm.prank(caller);
89+
idRegistry.transfer(to, svm.createUint256("deadline"), svm.createBytes(65, "sig"));
90+
91+
// Record post-state
92+
uint256 newIdCaller = idRegistry.idOf(caller);
93+
uint256 newIdTo = idRegistry.idOf(to);
94+
uint256 newIdOther = idRegistry.idOf(other);
95+
96+
// Ensure that the fid has been transferred from the `caller` to the `to`.
97+
assert(newIdTo == oldIdCaller);
98+
if (caller != to) {
99+
assert(oldIdCaller != 0 && newIdCaller == 0);
100+
assert(oldIdTo == 0 && newIdTo != 0);
101+
}
102+
103+
// Ensure that the other fids are not affected.
104+
assert(newIdOther == oldIdOther);
105+
}
106+
107+
function check_Recovery(address caller, address from, address to, address other) public {
108+
_initState();
109+
vm.assume(other != from && other != to);
110+
111+
// Record pre-state
112+
uint256 oldIdFrom = idRegistry.idOf(from);
113+
uint256 oldIdTo = idRegistry.idOf(to);
114+
uint256 oldIdOther = idRegistry.idOf(other);
115+
address oldRecoveryFrom = idRegistry.recoveryOf(oldIdFrom);
116+
117+
// Execute recover with symbolic arguments
118+
vm.prank(caller);
119+
idRegistry.recover(from, to, svm.createUint256("deadline"), svm.createBytes(65, "sig"));
120+
121+
// Record post-state
122+
uint256 newIdFrom = idRegistry.idOf(from);
123+
uint256 newIdTo = idRegistry.idOf(to);
124+
uint256 newIdOther = idRegistry.idOf(other);
125+
126+
// Ensure that the caller is the recovery address
127+
assert(caller == oldRecoveryFrom);
128+
129+
// Ensure that the fid has been transferred from the `from` to the `to`.
130+
assert(newIdTo == oldIdFrom);
131+
if (from != to) {
132+
assert(oldIdFrom != 0 && newIdFrom == 0);
133+
assert(oldIdTo == 0 && newIdTo != 0);
134+
}
135+
136+
// Ensure that the other fids are not affected.
137+
assert(newIdOther == oldIdOther);
138+
}
139+
140+
/*//////////////////////////////////////////////////////////////
141+
HELPERS
142+
//////////////////////////////////////////////////////////////*/
143+
144+
/**
145+
* @dev Initialize IdRegistry with symbolic arguments for state.
146+
*/
147+
function _initState() public {
148+
if (svm.createBool("disableTrustedOnly?")) {
149+
idRegistry.disableTrustedOnly();
150+
}
151+
if (svm.createBool("pause?")) {
152+
idRegistry.pause();
153+
}
154+
}
155+
156+
/**
157+
* @dev Generates valid calldata for a given function selector.
158+
*/
159+
function _calldataFor(bytes4 selector) internal returns (bytes memory) {
160+
bytes memory args;
161+
if (selector == idRegistry.registerFor.selector) {
162+
args = abi.encode(
163+
svm.createAddress("to"),
164+
svm.createAddress("recovery"),
165+
svm.createUint256("deadline"),
166+
svm.createBytes(65, "sig")
167+
);
168+
} else if (selector == idRegistry.transfer.selector) {
169+
args = abi.encode(svm.createAddress("to"), svm.createUint256("deadline"), svm.createBytes(65, "sig"));
170+
} else if (selector == idRegistry.transferFor.selector) {
171+
args = abi.encode(
172+
svm.createAddress("from"),
173+
svm.createAddress("to"),
174+
svm.createUint256("fromDeadline"),
175+
svm.createBytes(65, "fromSig"),
176+
svm.createUint256("toDeadline"),
177+
svm.createBytes(65, "toSig")
178+
);
179+
} else if (selector == idRegistry.changeRecoveryAddressFor.selector) {
180+
args = abi.encode(
181+
svm.createAddress("owner"),
182+
svm.createAddress("recovery"),
183+
svm.createUint256("deadline"),
184+
svm.createBytes(65, "sig")
185+
);
186+
} else if (selector == idRegistry.recover.selector) {
187+
args = abi.encode(
188+
svm.createAddress("from"),
189+
svm.createAddress("to"),
190+
svm.createUint256("deadline"),
191+
svm.createBytes(65, "sig")
192+
);
193+
} else if (selector == idRegistry.recoverFor.selector) {
194+
args = abi.encode(
195+
svm.createAddress("from"),
196+
svm.createAddress("to"),
197+
svm.createUint256("recoveryDeadline"),
198+
svm.createBytes(65, "recoverySig"),
199+
svm.createUint256("toDeadline"),
200+
svm.createBytes(65, "toSig")
201+
);
202+
} else if (selector == idRegistry.verifyFidSignature.selector) {
203+
args = abi.encode(
204+
svm.createAddress("custodyAddress"),
205+
svm.createUint256("fid"),
206+
svm.createBytes32("digest"),
207+
svm.createBytes(65, "sig")
208+
);
209+
} else {
210+
args = svm.createBytes(1024, "data");
211+
}
212+
213+
return abi.encodePacked(selector, args);
214+
}
215+
}

0 commit comments

Comments
 (0)