Skip to content

Commit 1ec94b1

Browse files
committed
Add symbolic tests with Halmos
1 parent eb4fad6 commit 1ec94b1

File tree

2 files changed

+367
-0
lines changed

2 files changed

+367
-0
lines changed

.github/workflows/run_halmos.yml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: ERC721A CI - Halmos
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths-ignore:
7+
- 'docs/**'
8+
- '**.md'
9+
pull_request:
10+
branches: [main]
11+
paths-ignore:
12+
- 'docs/**'
13+
- '**.md'
14+
15+
jobs:
16+
run-halmos:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v2
20+
- name: Use Node.js 16.x
21+
uses: actions/setup-node@v2
22+
with:
23+
node-version: 16.x
24+
- name: Install dependencies
25+
run: npm ci
26+
- name: Install halmos
27+
run: python3 -m pip install --upgrade halmos
28+
- name: Run halmos
29+
run: |
30+
sed -i 's/\bprivate\b/internal/g' contracts/ERC721A.sol
31+
cp test/halmos/ERC721A.t.sol contracts/
32+
halmos

test/halmos/ERC721A.t.sol

+335
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.4;
3+
4+
import './ERC721A.sol';
5+
6+
contract ERC721ATest is ERC721A {
7+
8+
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) { }
9+
10+
function isBurned(uint tokenId) public view returns (bool) {
11+
return _packedOwnershipOf(tokenId) & _BITMASK_BURNED != 0;
12+
}
13+
14+
//
15+
// mint
16+
//
17+
18+
// TODO: duplicate spec for _mintERC2309 and _safeMint
19+
function mint(address to, uint quantity) public {
20+
_mint(to, quantity);
21+
//_mintERC2309(to, quantity);
22+
//_safeMint(to, quantity);
23+
//_safeMint(to, quantity, data);
24+
}
25+
26+
function testMintRequirements(address to, uint quantity) public {
27+
mint(to, quantity);
28+
29+
assert(to != address(0));
30+
assert(quantity > 0);
31+
}
32+
33+
function testMintNextTokenIdUpdate(address to, uint quantity) public {
34+
uint oldNextTokenId = _nextTokenId();
35+
require(oldNextTokenId <= type(uint96).max); // practical assumption needed for overflow/underflow not occurring
36+
37+
mint(to, quantity);
38+
39+
uint newNextTokenId = _nextTokenId();
40+
41+
assert(newNextTokenId >= oldNextTokenId); // ensuring no overflow
42+
assert(newNextTokenId == oldNextTokenId + quantity);
43+
}
44+
45+
function testMintBalanceUpdate(address to, uint quantity) public {
46+
uint oldBalanceTo = balanceOf(to);
47+
require(oldBalanceTo <= type(uint64).max / 2); // practical assumption needed for balance staying within uint64
48+
49+
mint(to, quantity);
50+
51+
uint newBalanceTo = balanceOf(to);
52+
53+
assert(newBalanceTo >= oldBalanceTo); // ensuring no overflow
54+
assert(newBalanceTo == oldBalanceTo + quantity);
55+
}
56+
57+
function testMintOwnershipUpdate(address to, uint quantity, uint _newNextTokenId) public {
58+
uint oldNextTokenId = _nextTokenId();
59+
require(oldNextTokenId <= type(uint96).max); // practical assumption needed for overflow/underflow not occurring
60+
61+
// global invariant
62+
for (uint i = oldNextTokenId; i < _newNextTokenId; i++) {
63+
require(_packedOwnerships[i] == 0); // assumption for uninitialized mappings (i.e., no hash collision for hashed storage addresses)
64+
}
65+
66+
mint(to, quantity);
67+
68+
uint newNextTokenId = _nextTokenId();
69+
require(_newNextTokenId == newNextTokenId);
70+
71+
for (uint i = oldNextTokenId; i < newNextTokenId; i++) {
72+
assert(ownerOf(i) == to);
73+
assert(!isBurned(i));
74+
}
75+
}
76+
77+
function testMintOtherBalancePreservation(address to, uint quantity, address others) public {
78+
require(others != to); // consider other addresses
79+
80+
uint oldBalanceOther = balanceOf(others);
81+
82+
mint(to, quantity);
83+
84+
uint newBalanceOther = balanceOf(others);
85+
86+
assert(newBalanceOther == oldBalanceOther); // the balance of other addresses never change
87+
}
88+
89+
function testMintOtherOwnershipPreservation(address to, uint quantity, uint existingTokenId) public {
90+
uint oldNextTokenId = _nextTokenId();
91+
require(oldNextTokenId <= type(uint96).max); // practical assumption needed for overflow/underflow not occurring
92+
93+
require(existingTokenId < oldNextTokenId); // consider other token ids
94+
95+
address oldOwnerExisting = ownerOf(existingTokenId);
96+
bool oldBurned = isBurned(existingTokenId);
97+
98+
mint(to, quantity);
99+
100+
address newOwnerExisting = ownerOf(existingTokenId);
101+
bool newBurned = isBurned(existingTokenId);
102+
103+
assert(newOwnerExisting == oldOwnerExisting); // the owner of other token ids never change
104+
assert(newBurned == oldBurned);
105+
}
106+
107+
//
108+
// burn
109+
//
110+
111+
// TODO: duplicate spec for both modes
112+
function burn(uint tokenId) public {
113+
//_burn(tokenId, true);
114+
_burn(tokenId, false);
115+
}
116+
117+
function testBurnRequirements(uint tokenId) public {
118+
// TODO: prove global invariant
119+
require(!(_packedOwnerships[tokenId] == 0) || !isBurned(tokenId));
120+
require(!(_packedOwnerships[tokenId] != 0) || tokenId < _nextTokenId());
121+
122+
bool exist = _exists(tokenId);
123+
bool burned = isBurned(tokenId);
124+
125+
address owner = ownerOf(tokenId);
126+
bool approved = msg.sender == _tokenApprovals[tokenId].value || isApprovedForAll(owner, msg.sender);
127+
128+
_burn(tokenId, true);
129+
130+
assert(exist); // it should have reverted if the token id does not exist
131+
assert(!burned);
132+
133+
assert(msg.sender == owner || approved);
134+
135+
assert(_tokenApprovals[tokenId].value == address(0)); // getApproved(tokenId) reverts here
136+
}
137+
138+
function testBurnNextTokenIdUnchanged(uint tokenId) public {
139+
uint oldNextTokenId = _nextTokenId();
140+
141+
burn(tokenId);
142+
143+
uint newNextTokenId = _nextTokenId();
144+
145+
assert(newNextTokenId == oldNextTokenId);
146+
}
147+
148+
function testBurnBalanceUpdate(uint tokenId) public {
149+
address from = ownerOf(tokenId);
150+
uint oldBalanceFrom = balanceOf(from);
151+
152+
// TODO: prove global invariant
153+
require(!(_packedOwnerships[tokenId] != 0) || tokenId < _nextTokenId());
154+
require(!_exists(tokenId) || oldBalanceFrom > 0);
155+
156+
burn(tokenId);
157+
158+
uint newBalanceFrom = balanceOf(from);
159+
160+
assert(newBalanceFrom < oldBalanceFrom); // ensuring no overflow
161+
assert(newBalanceFrom == oldBalanceFrom - 1);
162+
}
163+
164+
function testBurnOwnershipUpdate(uint tokenId) public {
165+
burn(tokenId);
166+
167+
assert(!_exists(tokenId));
168+
assert(_packedOwnerships[tokenId] & _BITMASK_BURNED != 0); // isBurned reverts here
169+
}
170+
171+
function testBurnOtherBalancePreservation(uint tokenId, address others) public {
172+
address from = ownerOf(tokenId);
173+
require(others != from); // consider other addresses
174+
175+
uint oldBalanceOther = balanceOf(others);
176+
177+
burn(tokenId);
178+
179+
uint newBalanceOther = balanceOf(others);
180+
181+
assert(newBalanceOther == oldBalanceOther);
182+
}
183+
184+
function testBurnOtherOwnershipPreservation(uint tokenId, uint otherTokenId) public {
185+
require(_nextTokenId() <= type(uint96).max); // practical assumption needed for avoiding overflow/underflow
186+
187+
// TODO: prove global invariant
188+
// global invariant
189+
require(!(_packedOwnerships[tokenId] == 0) || _packedOwnershipOf(tokenId) & _BITMASK_NEXT_INITIALIZED == 0);
190+
require(!(_packedOwnerships[tokenId] != 0) || tokenId < _nextTokenId());
191+
require(!(_packedOwnershipOf(tokenId) & _BITMASK_NEXT_INITIALIZED != 0)
192+
|| !(tokenId + 1 < _nextTokenId())
193+
|| _packedOwnerships[tokenId + 1] != 0);
194+
195+
require(otherTokenId != tokenId); // consider other token ids
196+
197+
address oldOtherTokenOwner = ownerOf(otherTokenId);
198+
bool oldBurned = isBurned(otherTokenId);
199+
200+
burn(tokenId);
201+
202+
address newOtherTokenOwner = ownerOf(otherTokenId);
203+
bool newBurned = isBurned(otherTokenId);
204+
205+
assert(newOtherTokenOwner == oldOtherTokenOwner);
206+
assert(newBurned == oldBurned);
207+
}
208+
209+
//
210+
// transfer
211+
//
212+
213+
// TODO: duplicate spec for safeTransferFrom
214+
function transfer(address from, address to, uint tokenId) public {
215+
transferFrom(from, to, tokenId);
216+
//safeTransferFrom(from, to, tokenId);
217+
//safeTransferFrom(from, to, tokenId, data);
218+
}
219+
220+
function testTransferRequirements(address from, address to, uint tokenId) public {
221+
// TODO: prove global invariant
222+
require(!(_packedOwnerships[tokenId] == 0) || !isBurned(tokenId));
223+
require(!(_packedOwnerships[tokenId] != 0) || tokenId < _nextTokenId());
224+
225+
bool exist = _exists(tokenId);
226+
bool burned = isBurned(tokenId);
227+
228+
address owner = ownerOf(tokenId);
229+
bool approved = msg.sender == _tokenApprovals[tokenId].value || isApprovedForAll(owner, msg.sender);
230+
231+
transfer(from, to, tokenId);
232+
233+
assert(exist); // it should have reverted if the token id does not exist
234+
assert(!burned);
235+
236+
//assert(from != address(0)); // NOTE: ERC721A doesn't explicitly check this condition
237+
assert(to != address(0));
238+
239+
assert(from == owner);
240+
assert(msg.sender == owner || approved);
241+
242+
assert(_tokenApprovals[tokenId].value == address(0));
243+
}
244+
245+
function testTransferNextTokenIdUnchanged(address from, address to, uint tokenId) public {
246+
uint oldNextTokenId = _nextTokenId();
247+
248+
transfer(from, to, tokenId);
249+
250+
uint newNextTokenId = _nextTokenId();
251+
252+
assert(newNextTokenId == oldNextTokenId);
253+
}
254+
255+
function testTransferBalanceUpdate(address from, address to, uint tokenId) public {
256+
require(from != to); // consider normal transfer case (see below for the self-transfer case)
257+
258+
uint oldBalanceFrom = balanceOf(from);
259+
uint oldBalanceTo = balanceOf(to);
260+
261+
require(oldBalanceTo <= type(uint64).max / 2); // practical assumption needed for balance staying within uint64
262+
263+
// TODO: prove global invariant
264+
require(!(_packedOwnerships[tokenId] != 0) || tokenId < _nextTokenId());
265+
require(!_exists(tokenId) || balanceOf(ownerOf(tokenId)) > 0);
266+
267+
transfer(from, to, tokenId);
268+
269+
uint newBalanceFrom = balanceOf(from);
270+
uint newBalanceTo = balanceOf(to);
271+
272+
assert(newBalanceFrom < oldBalanceFrom);
273+
assert(newBalanceFrom == oldBalanceFrom - 1);
274+
275+
assert(newBalanceTo > oldBalanceTo);
276+
assert(newBalanceTo == oldBalanceTo + 1);
277+
}
278+
279+
function testTransferBalanceUnchanged(address from, address to, uint tokenId) public {
280+
require(from == to); // consider self-transfer case
281+
282+
uint oldBalance = balanceOf(from); // == balanceOf(to);
283+
284+
transfer(from, to, tokenId);
285+
286+
uint newBalance = balanceOf(from); // == balanceOf(to);
287+
288+
assert(newBalance == oldBalance);
289+
}
290+
291+
function testTransferOwnershipUpdate(address from, address to, uint tokenId) public {
292+
transfer(from, to, tokenId);
293+
294+
assert(ownerOf(tokenId) == to);
295+
assert(!isBurned(tokenId));
296+
}
297+
298+
function testTransferOtherBalancePreservation(address from, address to, uint tokenId, address others) public {
299+
require(others != from); // consider other addresses
300+
require(others != to);
301+
302+
uint oldBalanceOther = balanceOf(others);
303+
304+
transfer(from, to, tokenId);
305+
306+
uint newBalanceOther = balanceOf(others);
307+
308+
assert(newBalanceOther == oldBalanceOther);
309+
}
310+
311+
function testTransferOtherOwnershipPreservation(address from, address to, uint tokenId, uint otherTokenId) public {
312+
require(_nextTokenId() <= type(uint96).max); // practical assumption needed for avoiding overflow/underflow
313+
314+
// TODO: prove global invariant
315+
// global invariant
316+
require(!(_packedOwnerships[tokenId] == 0) || _packedOwnershipOf(tokenId) & _BITMASK_NEXT_INITIALIZED == 0);
317+
require(!(_packedOwnerships[tokenId] != 0) || tokenId < _nextTokenId());
318+
require(!(_packedOwnershipOf(tokenId) & _BITMASK_NEXT_INITIALIZED != 0)
319+
|| !(tokenId + 1 < _nextTokenId())
320+
|| _packedOwnerships[tokenId + 1] != 0);
321+
322+
require(otherTokenId != tokenId); // consider other token ids
323+
324+
address oldOtherTokenOwner = ownerOf(otherTokenId);
325+
bool oldBurned = isBurned(otherTokenId);
326+
327+
transfer(from, to, tokenId);
328+
329+
address newOtherTokenOwner = ownerOf(otherTokenId);
330+
bool newBurned = isBurned(otherTokenId);
331+
332+
assert(newOtherTokenOwner == oldOtherTokenOwner);
333+
assert(newBurned == oldBurned);
334+
}
335+
}

0 commit comments

Comments
 (0)