Skip to content

Commit 5c0a47b

Browse files
committed
fix: unit tests for BCR
1 parent 1623e6f commit 5c0a47b

File tree

5 files changed

+246
-11
lines changed

5 files changed

+246
-11
lines changed

contracts/global/BytecodeRepository.sol

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ contract BytecodeRepository is ImmutableOwnableTrait, SanityCheckTrait, IBytecod
6363
bytes32 public constant BYTECODE_TYPEHASH =
6464
keccak256("Bytecode(bytes32 contractType,uint256 version,bytes initCode,address author,string source)");
6565

66-
bytes32 public constant _SIGNATURE_TYPEHASH = keccak256("SignBytecodeHash(bytes32 bytecodeHash,string reportUrl)");
66+
bytes32 public constant AUDITOR_SIGNATURE_TYPEHASH =
67+
keccak256("SignBytecodeHash(bytes32 bytecodeHash,string reportUrl)");
6768

6869
//
6970
// STORAGE
@@ -269,7 +270,8 @@ contract BytecodeRepository is ImmutableOwnableTrait, SanityCheckTrait, IBytecod
269270
}
270271

271272
// Re-create typed data
272-
bytes32 structHash = keccak256(abi.encode(_SIGNATURE_TYPEHASH, bytecodeHash, keccak256(bytes(reportUrl))));
273+
bytes32 structHash =
274+
keccak256(abi.encode(AUDITOR_SIGNATURE_TYPEHASH, bytecodeHash, keccak256(bytes(reportUrl))));
273275
// Hash with our pinned domain
274276
address signer = ECDSA.recover(_hashTypedDataV4(structHash), signature);
275277

@@ -539,4 +541,8 @@ contract BytecodeRepository is ImmutableOwnableTrait, SanityCheckTrait, IBytecod
539541
function bytecodeByHash(bytes32 bytecodeHash) external view returns (Bytecode memory) {
540542
return _bytecodeByHash[bytecodeHash];
541543
}
544+
545+
function domainSeparatorV4() external view returns (bytes32) {
546+
return _domainSeparatorV4();
547+
}
542548
}

contracts/global/CrossChainMultisig.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,6 @@ contract CrossChainMultisig is EIP712Mainnet, Ownable, ReentrancyGuard, ICrossCh
149149
_executeProposal({calls: signedProposal.calls, proposalHash: proposalHash});
150150
}
151151

152-
function domainSeparator() external view returns (bytes32) {
153-
return _domainSeparatorV4();
154-
}
155-
156152
function _verifyProposal(CrossChainCall[] memory calls, bytes32 prevHash) internal view {
157153
if (prevHash != lastProposalHash) revert InvalidPrevHashException();
158154
if (calls.length == 0) revert NoCallsInProposalException();
@@ -312,4 +308,8 @@ contract CrossChainMultisig is EIP712Mainnet, Ownable, ReentrancyGuard, ICrossCh
312308
function signedProposals(bytes32 proposalHash) external view returns (SignedProposal memory) {
313309
return _signedProposals[proposalHash];
314310
}
311+
312+
function domainSeparatorV4() external view returns (bytes32) {
313+
return _domainSeparatorV4();
314+
}
315315
}

contracts/interfaces/ICrossChainMultisig.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,5 @@ interface ICrossChainMultisig is IVersion {
138138
function isSigner(address account) external view returns (bool);
139139

140140
/// @notice Returns the domain separator used for EIP-712 signing
141-
function domainSeparator() external view returns (bytes32);
141+
function domainSeparatorV4() external view returns (bytes32);
142142
}

contracts/test/global/BytecodeRepository.uint.t.sol

Lines changed: 231 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ contract BytecodeRepositoryTest is Test {
2020
address public auditor;
2121
address public author;
2222

23+
uint256 public auditorPK = vm.randomUint();
24+
2325
uint256 public authorPK = vm.randomUint();
2426

2527
bytes32 private constant _TEST_CONTRACT = "TEST_CONTRACT";
@@ -29,16 +31,243 @@ contract BytecodeRepositoryTest is Test {
2931

3032
function setUp() public {
3133
owner = makeAddr("owner");
32-
auditor = makeAddr("auditor");
34+
auditor = vm.addr(auditorPK);
3335
author = vm.addr(authorPK);
3436

3537
vm.startPrank(owner);
36-
repository = new BytecodeRepository(address(this));
38+
repository = new BytecodeRepository(owner);
3739
repository.addAuditor(auditor, "Test Auditor");
3840
vm.stopPrank();
3941
}
4042

4143
function _getMockBytecode(bytes32 _contractType, uint256 _version) internal pure returns (bytes memory) {
4244
return abi.encodePacked(type(MockedVersionContract).creationCode, abi.encode(_contractType, _version));
4345
}
46+
47+
function _uploadTestBytecode() internal returns (bytes32 bytecodeHash) {
48+
bytes memory bytecode = _getMockBytecode(_TEST_CONTRACT, _TEST_VERSION);
49+
50+
Bytecode memory bc = Bytecode({
51+
contractType: _TEST_CONTRACT,
52+
version: _TEST_VERSION,
53+
initCode: bytecode,
54+
author: author,
55+
source: _TEST_SOURCE,
56+
authorSignature: bytes("")
57+
});
58+
59+
bytecodeHash = repository.computeBytecodeHash(bc);
60+
61+
(uint8 v, bytes32 r, bytes32 s) =
62+
vm.sign(authorPK, repository.domainSeparatorV4().toTypedDataHash(bytecodeHash));
63+
bc.authorSignature = abi.encodePacked(r, s, v);
64+
65+
vm.prank(author);
66+
repository.uploadBytecode(bc);
67+
}
68+
69+
/// UPLOAD BYTECODE TESTS
70+
71+
function test_BCR_01_uploadBytecode_works() public {
72+
bytes memory bytecode = _getMockBytecode(_TEST_CONTRACT, _TEST_VERSION);
73+
74+
Bytecode memory bc = Bytecode({
75+
contractType: _TEST_CONTRACT,
76+
version: _TEST_VERSION,
77+
initCode: bytecode,
78+
author: author,
79+
source: _TEST_SOURCE,
80+
authorSignature: bytes("")
81+
});
82+
83+
bytes32 bytecodeHash = repository.computeBytecodeHash(bc);
84+
85+
// Sign bytecode hash with author's key
86+
(uint8 v, bytes32 r, bytes32 s) =
87+
vm.sign(authorPK, repository.domainSeparatorV4().toTypedDataHash(bytecodeHash));
88+
bc.authorSignature = abi.encodePacked(r, s, v);
89+
90+
vm.prank(author);
91+
repository.uploadBytecode(bc);
92+
93+
// Verify bytecode was stored
94+
assertTrue(repository.isBytecodeUploaded(bytecodeHash));
95+
96+
Bytecode memory storedBc = repository.bytecodeByHash(bytecodeHash);
97+
assertEq(storedBc.contractType, _TEST_CONTRACT);
98+
assertEq(storedBc.version, _TEST_VERSION);
99+
assertEq(storedBc.author, author);
100+
assertEq(storedBc.source, _TEST_SOURCE);
101+
}
102+
103+
function test_BCR_02_uploadBytecode_reverts_if_already_exists() public {
104+
bytes memory bytecode = _getMockBytecode(_TEST_CONTRACT, _TEST_VERSION);
105+
106+
Bytecode memory bc = Bytecode({
107+
contractType: _TEST_CONTRACT,
108+
version: _TEST_VERSION,
109+
initCode: bytecode,
110+
author: author,
111+
source: _TEST_SOURCE,
112+
authorSignature: bytes("")
113+
});
114+
115+
bytes32 bytecodeHash = repository.computeBytecodeHash(bc);
116+
117+
(uint8 v, bytes32 r, bytes32 s) =
118+
vm.sign(authorPK, repository.domainSeparatorV4().toTypedDataHash(bytecodeHash));
119+
bc.authorSignature = abi.encodePacked(r, s, v);
120+
121+
vm.startPrank(author);
122+
repository.uploadBytecode(bc);
123+
124+
vm.expectRevert(IBytecodeRepository.BytecodeAlreadyExistsException.selector);
125+
repository.uploadBytecode(bc);
126+
vm.stopPrank();
127+
}
128+
129+
function test_BCR_03_uploadBytecode_reverts_if_invalid_signature() public {
130+
bytes memory bytecode = _getMockBytecode(_TEST_CONTRACT, _TEST_VERSION);
131+
132+
Bytecode memory bc = Bytecode({
133+
contractType: _TEST_CONTRACT,
134+
version: _TEST_VERSION,
135+
initCode: bytecode,
136+
author: author,
137+
source: _TEST_SOURCE,
138+
authorSignature: bytes("invalid signature")
139+
});
140+
141+
vm.prank(author);
142+
vm.expectRevert("ECDSA: invalid signature length");
143+
repository.uploadBytecode(bc);
144+
}
145+
146+
/// AUDITOR SIGNATURE TESTS
147+
148+
function test_BCR_04_signBytecodeHash_works() public {
149+
// First upload bytecode
150+
bytes32 bytecodeHash = _uploadTestBytecode();
151+
152+
// Now sign as auditor
153+
string memory reportUrl = "https://audit.report";
154+
bytes32 signatureHash = repository.domainSeparatorV4().toTypedDataHash(
155+
keccak256(abi.encode(repository.AUDITOR_SIGNATURE_TYPEHASH(), bytecodeHash, keccak256(bytes(reportUrl))))
156+
);
157+
158+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(auditorPK, signatureHash);
159+
bytes memory signature = abi.encodePacked(r, s, v);
160+
161+
vm.prank(auditor);
162+
repository.signBytecodeHash(bytecodeHash, reportUrl, signature);
163+
164+
// Verify signature was stored
165+
assertTrue(repository.isBytecodeAudited(bytecodeHash));
166+
167+
AuditorSignature[] memory sigs = repository.auditorSignaturesByHash(bytecodeHash);
168+
assertEq(sigs.length, 1);
169+
assertEq(sigs[0].auditor, auditor);
170+
assertEq(sigs[0].reportUrl, reportUrl);
171+
assertEq(sigs[0].signature, signature);
172+
}
173+
174+
function test_BCR_05_signBytecodeHash_reverts_if_not_auditor() public {
175+
// First upload bytecode
176+
bytes32 bytecodeHash = _uploadTestBytecode();
177+
178+
// Now sign as auditor
179+
string memory reportUrl = "https://audit.report";
180+
bytes32 signatureHash = repository.domainSeparatorV4().toTypedDataHash(
181+
keccak256(abi.encode(repository.AUDITOR_SIGNATURE_TYPEHASH(), bytecodeHash, keccak256(bytes(reportUrl))))
182+
);
183+
184+
uint256 notAuditorPK = vm.randomUint();
185+
address notAuditor = vm.addr(notAuditorPK);
186+
187+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(notAuditorPK, signatureHash);
188+
bytes memory signature = abi.encodePacked(r, s, v);
189+
190+
vm.prank(notAuditor);
191+
vm.expectRevert(abi.encodeWithSelector(IBytecodeRepository.SignerIsNotAuditorException.selector, notAuditor));
192+
repository.signBytecodeHash(bytecodeHash, reportUrl, signature);
193+
}
194+
195+
/// DEPLOYMENT TESTS
196+
197+
function test_BCR_06_deploy_works() public {
198+
// First upload bytecode
199+
bytes32 bytecodeHash = _uploadTestBytecode();
200+
201+
// Now sign as auditor
202+
string memory reportUrl = "https://audit.report";
203+
bytes32 signatureHash = repository.domainSeparatorV4().toTypedDataHash(
204+
keccak256(abi.encode(repository.AUDITOR_SIGNATURE_TYPEHASH(), bytecodeHash, keccak256(bytes(reportUrl))))
205+
);
206+
207+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(auditorPK, signatureHash);
208+
bytes memory signature = abi.encodePacked(r, s, v);
209+
210+
vm.prank(auditor);
211+
repository.signBytecodeHash(bytecodeHash, reportUrl, signature);
212+
213+
// Mark as system contract to auto-approve
214+
vm.prank(owner);
215+
repository.allowSystemContract(bytecodeHash);
216+
217+
// Now deploy
218+
address deployer = makeAddr("deployer");
219+
vm.prank(deployer);
220+
address deployed = repository.deploy(_TEST_CONTRACT, _TEST_VERSION, "", _TEST_SALT);
221+
222+
// Verify deployment
223+
assertTrue(deployed.code.length > 0);
224+
assertEq(repository.deployedContracts(deployed), bytecodeHash);
225+
226+
IVersion version = IVersion(deployed);
227+
assertEq(version.contractType(), _TEST_CONTRACT);
228+
assertEq(version.version(), _TEST_VERSION);
229+
}
230+
231+
function test_BCR_07_deploy_reverts_if_not_approved() public {
232+
vm.expectRevert(
233+
abi.encodeWithSelector(
234+
IBytecodeRepository.BytecodeIsNotApprovedException.selector, _TEST_CONTRACT, _TEST_VERSION
235+
)
236+
);
237+
repository.deploy(_TEST_CONTRACT, _TEST_VERSION, "", _TEST_SALT);
238+
}
239+
240+
function test_BCR_08_deploy_reverts_if_not_audited() public {
241+
// Upload but don't audit
242+
bytes memory bytecode = _getMockBytecode(_TEST_CONTRACT, _TEST_VERSION);
243+
244+
Bytecode memory bc = Bytecode({
245+
contractType: _TEST_CONTRACT,
246+
version: _TEST_VERSION,
247+
initCode: bytecode,
248+
author: author,
249+
source: _TEST_SOURCE,
250+
authorSignature: bytes("")
251+
});
252+
253+
bytes32 bytecodeHash = repository.computeBytecodeHash(bc);
254+
255+
(uint8 v, bytes32 r, bytes32 s) =
256+
vm.sign(authorPK, repository.domainSeparatorV4().toTypedDataHash(bytecodeHash));
257+
bc.authorSignature = abi.encodePacked(r, s, v);
258+
259+
vm.prank(author);
260+
repository.uploadBytecode(bc);
261+
262+
// Mark as system contract to auto-approve
263+
vm.prank(owner);
264+
repository.allowSystemContract(bytecodeHash);
265+
266+
vm.expectRevert(
267+
abi.encodeWithSelector(
268+
IBytecodeRepository.BytecodeIsNotApprovedException.selector, _TEST_CONTRACT, _TEST_VERSION
269+
)
270+
);
271+
repository.deploy(_TEST_CONTRACT, _TEST_VERSION, "", _TEST_SALT);
272+
}
44273
}

contracts/test/global/CrossChainMultisig.unit.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ contract CrossChainMultisigTest is Test {
3737
}
3838

3939
function _getDigest(bytes32 structHash) internal view returns (bytes32) {
40-
bytes32 domainSeparator = multisig.domainSeparator();
40+
bytes32 domainSeparator = multisig.domainSeparatorV4();
4141
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
4242
}
4343

@@ -173,7 +173,7 @@ contract CrossChainMultisigTest is Test {
173173
bytes32 proposalHash = multisig.hashProposal(calls, bytes32(0));
174174

175175
// Generate EIP-712 signature
176-
bytes32 domainSeparator = multisig.domainSeparator();
176+
bytes32 domainSeparator = multisig.domainSeparatorV4();
177177
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, proposalHash));
178178

179179
bytes memory signature = _signProposalHash(signer0PrivateKey, proposalHash);

0 commit comments

Comments
 (0)