Skip to content

Commit 3c8ddcd

Browse files
committed
spike tests
1 parent 90535f8 commit 3c8ddcd

16 files changed

Lines changed: 909 additions & 0 deletions

test/lib/B20FactoryLibTest.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {BaseTest} from "test/lib/BaseTest.sol";
5+
6+
/// @notice Base test contract for `B20FactoryLib` unit tests.
7+
///
8+
/// `B20FactoryLib` is a pure encoder library — no storage, no
9+
/// precompile dispatch, no state. The library tests are correspondingly
10+
/// stateless: each test asserts that an encoder's output matches the
11+
/// hand-encoded `abi.encode` / `abi.encodeCall` form, or that a builder
12+
/// produces the expected `bytes[]` shape from typed inputs.
13+
///
14+
/// The base extends `BaseTest` purely for the shared actor pool
15+
/// (`admin`, `alice`, `bob`, `attacker`) and the `_assumeValidCaller`
16+
/// helper, even though the library itself does not consult `msg.sender`.
17+
/// Re-using those gives the test contracts a uniform vocabulary with
18+
/// the rest of the suite. No `setUp` extension is needed.
19+
contract B20FactoryLibTest is BaseTest {
20+
// No additional state; `BaseTest`'s actor labels and helpers are sufficient.
21+
}
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {B20FactoryLib} from "src/lib/B20FactoryLib.sol";
5+
import {B20Constants} from "src/lib/B20Constants.sol";
6+
import {IB20} from "src/interfaces/IB20.sol";
7+
8+
import {B20FactoryLibTest} from "test/lib/B20FactoryLibTest.sol";
9+
10+
contract B20FactoryLibBuildRoleGrantsTest is B20FactoryLibTest {
11+
/// @notice External wrapper that re-exposes
12+
/// `buildRoleGrants(bytes32[], address[])` for revert-path
13+
/// tests. Internal library calls inline into the test
14+
/// contract; `vm.expectRevert` requires the revert one
15+
/// CALL frame deeper, so revert tests must dispatch through
16+
/// an external entry point via `this.callBuildRoleGrants`.
17+
function callBuildRoleGrants(bytes32[] memory roles, address[] memory accounts)
18+
external
19+
pure
20+
returns (bytes[] memory)
21+
{
22+
return B20FactoryLib.buildRoleGrants(roles, accounts);
23+
}
24+
25+
/*//////////////////////////////////////////////////////////////
26+
REVERTS — buildRoleGrants(bytes32[], address[])
27+
//////////////////////////////////////////////////////////////*/
28+
29+
/// @notice Verifies the parallel-arrays overload reverts when
30+
/// `roles` and `accounts` differ in length.
31+
/// @dev The typed-bundle overloads bypass this check by
32+
/// construction; the raw overload is the only public
33+
/// entry point that can hit it.
34+
function test_buildRoleGrants_revert_lengthMismatch(uint8 rolesLenSeed, uint8 accountsLenSeed) public {
35+
uint256 rolesLen = bound(uint256(rolesLenSeed), 0, 32);
36+
uint256 accountsLen = bound(uint256(accountsLenSeed), 0, 32);
37+
vm.assume(rolesLen != accountsLen);
38+
bytes32[] memory roles = new bytes32[](rolesLen);
39+
address[] memory accounts = new address[](accountsLen);
40+
41+
vm.expectRevert(abi.encodeWithSelector(B20FactoryLib.LengthMismatch.selector, rolesLen, accountsLen));
42+
this.callBuildRoleGrants(roles, accounts);
43+
}
44+
45+
/*//////////////////////////////////////////////////////////////
46+
SUCCESS — buildRoleGrants(bytes32[], address[])
47+
//////////////////////////////////////////////////////////////*/
48+
49+
/// @notice Empty parallel arrays produce an empty result.
50+
/// @dev Boundary case for the length-zero allocation path.
51+
function test_buildRoleGrants_rawArrays_success_emptyInputProducesEmpty() public pure {
52+
bytes[] memory result = B20FactoryLib.buildRoleGrants(new bytes32[](0), new address[](0));
53+
assertEq(result.length, 0, "empty inputs must produce an empty result");
54+
}
55+
56+
/// @notice All-zero accounts produce no grants regardless of role count.
57+
/// @dev Pins the "leave a role unassigned at bootstrap" semantics
58+
/// that the typed overloads inherit from this primitive.
59+
function test_buildRoleGrants_rawArrays_success_allZeroAccountsProducesEmpty(uint8 lenSeed) public pure {
60+
uint256 len = bound(uint256(lenSeed), 0, 16);
61+
bytes32[] memory roles = new bytes32[](len);
62+
address[] memory accounts = new address[](len);
63+
for (uint256 i = 0; i < len; i++) {
64+
roles[i] = keccak256(abi.encode("role", i));
65+
}
66+
67+
bytes[] memory result = B20FactoryLib.buildRoleGrants(roles, accounts);
68+
assertEq(result.length, 0, "all-zero accounts must produce no grants");
69+
}
70+
71+
/// @notice Output preserves input order for the kept entries and skips
72+
/// the zero-address slots.
73+
/// @dev Mixed-pattern coverage: alternating zero / non-zero accounts.
74+
/// The result must contain exactly the non-zero pairs, in input
75+
/// order, each encoded as `grantRole(role, account)`.
76+
function test_buildRoleGrants_rawArrays_success_skipsZeroAddressesAndPreservesOrder(
77+
bytes32 r0,
78+
bytes32 r1,
79+
bytes32 r2,
80+
address a0,
81+
address a2
82+
) public pure {
83+
vm.assume(a0 != address(0));
84+
vm.assume(a2 != address(0));
85+
86+
bytes32[] memory roles = new bytes32[](3);
87+
roles[0] = r0;
88+
roles[1] = r1;
89+
roles[2] = r2;
90+
91+
address[] memory accounts = new address[](3);
92+
accounts[0] = a0;
93+
accounts[1] = address(0);
94+
accounts[2] = a2;
95+
96+
bytes[] memory result = B20FactoryLib.buildRoleGrants(roles, accounts);
97+
98+
assertEq(result.length, 2, "exactly two non-zero entries must survive");
99+
assertEq(result[0], abi.encodeCall(IB20.grantRole, (r0, a0)), "first kept entry must use roles[0]/accounts[0]");
100+
assertEq(result[1], abi.encodeCall(IB20.grantRole, (r2, a2)), "second kept entry must use roles[2]/accounts[2]");
101+
}
102+
103+
/// @notice Every non-zero slot produces exactly one grant in array order.
104+
/// @dev Density boundary opposite the all-zero case.
105+
function test_buildRoleGrants_rawArrays_success_allNonZeroProducesFullSet(uint8 lenSeed) public pure {
106+
uint256 len = bound(uint256(lenSeed), 1, 16);
107+
bytes32[] memory roles = new bytes32[](len);
108+
address[] memory accounts = new address[](len);
109+
for (uint256 i = 0; i < len; i++) {
110+
roles[i] = keccak256(abi.encode("role", i));
111+
accounts[i] = address(uint160(uint256(keccak256(abi.encode("acc", i)))));
112+
}
113+
114+
bytes[] memory result = B20FactoryLib.buildRoleGrants(roles, accounts);
115+
116+
assertEq(result.length, len, "every non-zero entry must produce a grant");
117+
for (uint256 i = 0; i < len; i++) {
118+
assertEq(result[i], abi.encodeCall(IB20.grantRole, (roles[i], accounts[i])), "ordering must follow input");
119+
}
120+
}
121+
122+
/*//////////////////////////////////////////////////////////////
123+
SUCCESS — buildRoleGrants(B20RoleHolders memory)
124+
//////////////////////////////////////////////////////////////*/
125+
126+
/// @notice An all-zero `B20RoleHolders` bundle produces an empty result.
127+
/// @dev Pins the zero-skip semantics for the IB20 typed overload.
128+
function test_buildRoleGrants_b20Bundle_success_allZeroProducesEmpty() public pure {
129+
B20FactoryLib.B20RoleHolders memory holders;
130+
bytes[] memory result = B20FactoryLib.buildRoleGrants(holders);
131+
assertEq(result.length, 0, "zeroed bundle must produce no grants");
132+
}
133+
134+
/// @notice A fully-populated `B20RoleHolders` bundle emits the six
135+
/// grants in struct-field order.
136+
/// @dev Pins both the canonical role ordering and the field-to-role
137+
/// mapping (e.g. `minter` MUST map to `MINT_ROLE`, not
138+
/// `BURN_ROLE`). A swap of any two fields/roles would surface
139+
/// as a per-index mismatch below.
140+
function test_buildRoleGrants_b20Bundle_success_emitsAllSixInStructOrder(
141+
address minter_,
142+
address burner_,
143+
address burnBlocker_,
144+
address pauser_,
145+
address unpauser_,
146+
address metadataAdmin_
147+
) public pure {
148+
vm.assume(minter_ != address(0));
149+
vm.assume(burner_ != address(0));
150+
vm.assume(burnBlocker_ != address(0));
151+
vm.assume(pauser_ != address(0));
152+
vm.assume(unpauser_ != address(0));
153+
vm.assume(metadataAdmin_ != address(0));
154+
155+
B20FactoryLib.B20RoleHolders memory holders = B20FactoryLib.B20RoleHolders({
156+
minter: minter_,
157+
burner: burner_,
158+
burnBlocker: burnBlocker_,
159+
pauser: pauser_,
160+
unpauser: unpauser_,
161+
metadataAdmin: metadataAdmin_
162+
});
163+
164+
bytes[] memory result = B20FactoryLib.buildRoleGrants(holders);
165+
166+
assertEq(result.length, 6, "fully-populated bundle must emit six grants");
167+
assertEq(result[0], abi.encodeCall(IB20.grantRole, (B20Constants.MINT_ROLE, minter_)), "0: MINT_ROLE");
168+
assertEq(result[1], abi.encodeCall(IB20.grantRole, (B20Constants.BURN_ROLE, burner_)), "1: BURN_ROLE");
169+
assertEq(
170+
result[2],
171+
abi.encodeCall(IB20.grantRole, (B20Constants.BURN_BLOCKED_ROLE, burnBlocker_)),
172+
"2: BURN_BLOCKED_ROLE"
173+
);
174+
assertEq(result[3], abi.encodeCall(IB20.grantRole, (B20Constants.PAUSE_ROLE, pauser_)), "3: PAUSE_ROLE");
175+
assertEq(result[4], abi.encodeCall(IB20.grantRole, (B20Constants.UNPAUSE_ROLE, unpauser_)), "4: UNPAUSE_ROLE");
176+
assertEq(
177+
result[5],
178+
abi.encodeCall(IB20.grantRole, (B20Constants.METADATA_ROLE, metadataAdmin_)),
179+
"5: METADATA_ROLE"
180+
);
181+
}
182+
183+
/// @notice A `B20RoleHolders` bundle with mixed zero / non-zero holders
184+
/// emits only the non-zero entries in struct-field order.
185+
/// @dev Pins the mixed-skip behavior. Populates minter, pauser,
186+
/// metadataAdmin; leaves burner, burnBlocker, unpauser zero.
187+
function test_buildRoleGrants_b20Bundle_success_skipsZeroSlots(
188+
address minter_,
189+
address pauser_,
190+
address metadataAdmin_
191+
) public pure {
192+
vm.assume(minter_ != address(0));
193+
vm.assume(pauser_ != address(0));
194+
vm.assume(metadataAdmin_ != address(0));
195+
196+
B20FactoryLib.B20RoleHolders memory holders = B20FactoryLib.B20RoleHolders({
197+
minter: minter_,
198+
burner: address(0),
199+
burnBlocker: address(0),
200+
pauser: pauser_,
201+
unpauser: address(0),
202+
metadataAdmin: metadataAdmin_
203+
});
204+
205+
bytes[] memory result = B20FactoryLib.buildRoleGrants(holders);
206+
207+
assertEq(result.length, 3, "exactly the three non-zero entries must survive");
208+
assertEq(result[0], abi.encodeCall(IB20.grantRole, (B20Constants.MINT_ROLE, minter_)), "minter first");
209+
assertEq(result[1], abi.encodeCall(IB20.grantRole, (B20Constants.PAUSE_ROLE, pauser_)), "pauser second");
210+
assertEq(
211+
result[2],
212+
abi.encodeCall(IB20.grantRole, (B20Constants.METADATA_ROLE, metadataAdmin_)),
213+
"metadataAdmin third"
214+
);
215+
}
216+
217+
/*//////////////////////////////////////////////////////////////
218+
SUCCESS — buildRoleGrants(B20SecurityRoleHolders memory)
219+
//////////////////////////////////////////////////////////////*/
220+
221+
/// @notice An all-zero `B20SecurityRoleHolders` bundle produces an empty result.
222+
/// @dev Pins the zero-skip semantics for the security typed overload.
223+
function test_buildRoleGrants_securityBundle_success_allZeroProducesEmpty() public pure {
224+
B20FactoryLib.B20SecurityRoleHolders memory holders;
225+
bytes[] memory result = B20FactoryLib.buildRoleGrants(holders);
226+
assertEq(result.length, 0, "zeroed bundle must produce no grants");
227+
}
228+
229+
/// @notice A fully-populated `B20SecurityRoleHolders` bundle emits the
230+
/// eight grants in struct-field order.
231+
/// @dev Same field-to-role pinning as the IB20 bundle, extended to
232+
/// the security-only `burnFromOperator` and `securityOperator`
233+
/// slots. Catches any swap among the eight role IDs.
234+
function test_buildRoleGrants_securityBundle_success_emitsAllEightInStructOrder(
235+
address minter_,
236+
address burner_,
237+
address burnBlocker_,
238+
address burnFromOperator_,
239+
address pauser_,
240+
address unpauser_,
241+
address metadataAdmin_,
242+
address securityOperator_
243+
) public pure {
244+
vm.assume(minter_ != address(0));
245+
vm.assume(burner_ != address(0));
246+
vm.assume(burnBlocker_ != address(0));
247+
vm.assume(burnFromOperator_ != address(0));
248+
vm.assume(pauser_ != address(0));
249+
vm.assume(unpauser_ != address(0));
250+
vm.assume(metadataAdmin_ != address(0));
251+
vm.assume(securityOperator_ != address(0));
252+
253+
B20FactoryLib.B20SecurityRoleHolders memory holders = B20FactoryLib.B20SecurityRoleHolders({
254+
minter: minter_,
255+
burner: burner_,
256+
burnBlocker: burnBlocker_,
257+
burnFromOperator: burnFromOperator_,
258+
pauser: pauser_,
259+
unpauser: unpauser_,
260+
metadataAdmin: metadataAdmin_,
261+
securityOperator: securityOperator_
262+
});
263+
264+
bytes[] memory result = B20FactoryLib.buildRoleGrants(holders);
265+
266+
assertEq(result.length, 8, "fully-populated security bundle must emit eight grants");
267+
assertEq(result[0], abi.encodeCall(IB20.grantRole, (B20Constants.MINT_ROLE, minter_)), "0: MINT_ROLE");
268+
assertEq(result[1], abi.encodeCall(IB20.grantRole, (B20Constants.BURN_ROLE, burner_)), "1: BURN_ROLE");
269+
assertEq(
270+
result[2],
271+
abi.encodeCall(IB20.grantRole, (B20Constants.BURN_BLOCKED_ROLE, burnBlocker_)),
272+
"2: BURN_BLOCKED_ROLE"
273+
);
274+
assertEq(
275+
result[3],
276+
abi.encodeCall(IB20.grantRole, (B20Constants.BURN_FROM_ROLE, burnFromOperator_)),
277+
"3: BURN_FROM_ROLE"
278+
);
279+
assertEq(result[4], abi.encodeCall(IB20.grantRole, (B20Constants.PAUSE_ROLE, pauser_)), "4: PAUSE_ROLE");
280+
assertEq(result[5], abi.encodeCall(IB20.grantRole, (B20Constants.UNPAUSE_ROLE, unpauser_)), "5: UNPAUSE_ROLE");
281+
assertEq(
282+
result[6],
283+
abi.encodeCall(IB20.grantRole, (B20Constants.METADATA_ROLE, metadataAdmin_)),
284+
"6: METADATA_ROLE"
285+
);
286+
assertEq(
287+
result[7],
288+
abi.encodeCall(IB20.grantRole, (B20Constants.SECURITY_OPERATOR_ROLE, securityOperator_)),
289+
"7: SECURITY_OPERATOR_ROLE"
290+
);
291+
}
292+
293+
/// @notice A `B20SecurityRoleHolders` bundle with only the
294+
/// security-only slots populated emits exactly those two
295+
/// grants in struct-field order.
296+
/// @dev Pins that `BURN_FROM_ROLE` and `SECURITY_OPERATOR_ROLE`
297+
/// pick up the right slot positions and the IB20 slots are
298+
/// skipped when zero.
299+
function test_buildRoleGrants_securityBundle_success_emitsOnlySecurityOnlySlots(
300+
address burnFromOperator_,
301+
address securityOperator_
302+
) public pure {
303+
vm.assume(burnFromOperator_ != address(0));
304+
vm.assume(securityOperator_ != address(0));
305+
306+
B20FactoryLib.B20SecurityRoleHolders memory holders = B20FactoryLib.B20SecurityRoleHolders({
307+
minter: address(0),
308+
burner: address(0),
309+
burnBlocker: address(0),
310+
burnFromOperator: burnFromOperator_,
311+
pauser: address(0),
312+
unpauser: address(0),
313+
metadataAdmin: address(0),
314+
securityOperator: securityOperator_
315+
});
316+
317+
bytes[] memory result = B20FactoryLib.buildRoleGrants(holders);
318+
319+
assertEq(result.length, 2, "exactly the two security-only entries must survive");
320+
assertEq(
321+
result[0],
322+
abi.encodeCall(IB20.grantRole, (B20Constants.BURN_FROM_ROLE, burnFromOperator_)),
323+
"burnFromOperator first"
324+
);
325+
assertEq(
326+
result[1],
327+
abi.encodeCall(IB20.grantRole, (B20Constants.SECURITY_OPERATOR_ROLE, securityOperator_)),
328+
"securityOperator second"
329+
);
330+
}
331+
}

0 commit comments

Comments
 (0)