Skip to content

Commit 872eb83

Browse files
test: pin that a clone can clone through the same factory during initialize
`cloneAndInitialize` calls the clone after the clone exists and after `NewClone` is emitted, and the library holds no state, so that call may re-enter the factory. No fixture called out during `initialize`, so nothing reached the re-entrant path at all: every existing fixture stores, emits, returns or reverts. `TestCloneableNestedClone` clones a second implementation through the factory that is initializing it. One test pins that both clones land at their own derivation's address with their own bytes and that the two `NewClone` logs interleave as an indexer would see them, the outer first and the nested one sent by the outer clone. The other pins that a nested revert takes the whole outer deploy with it and leaves its `(deployer, salt)` free, by deploying there again rather than by reading a code length a revert has already rolled back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9b05a32 commit 872eb83

2 files changed

Lines changed: 119 additions & 2 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: LicenseRef-DCL-1.0
2+
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
3+
pragma solidity =0.8.25;
4+
5+
import {ICloneableV2} from "src/interface/ICloneableV2.sol";
6+
import {ICloneableFactoryV4} from "src/interface/ICloneableFactoryV4.sol";
7+
8+
/// Thrown by a second call to `TestCloneableNestedClone.initialize`.
9+
error TestCloneableNestedCloneAlreadyInitialized();
10+
11+
/// @title TestCloneableNestedClone
12+
/// @notice An `ICloneableV2` whose `initialize` clones a second implementation
13+
/// through the factory that is initializing it, so the factory is re-entered
14+
/// from inside the `initialize` call it made. Every other fixture only stores,
15+
/// emits, returns or reverts, so none of them can reach the re-entrant path at
16+
/// all.
17+
///
18+
/// `data` is `abi.encode(factory, innerImplementation, innerSalt, innerData)`,
19+
/// and the clone it deployed during its own initialization is `sInner`.
20+
contract TestCloneableNestedClone is ICloneableV2 {
21+
/// The clone this one deployed during its own initialization.
22+
address public sInner;
23+
24+
/// Whether `initialize` has already run on this clone. Written before the
25+
/// nested call, so the re-entrant deploy cannot slip back past the guard.
26+
bool public sInitialized;
27+
28+
/// @inheritdoc ICloneableV2
29+
function initialize(bytes memory data) external returns (bytes32) {
30+
if (sInitialized) {
31+
revert TestCloneableNestedCloneAlreadyInitialized();
32+
}
33+
sInitialized = true;
34+
(address factory, address innerImplementation, bytes32 innerSalt, bytes memory innerData) =
35+
abi.decode(data, (address, address, bytes32, bytes));
36+
sInner = ICloneableFactoryV4(factory).cloneDeterministicOpenSalt(innerImplementation, innerData, innerSalt);
37+
// Deliberately the literal, not `ICLONEABLE_V2_SUCCESS`, as in
38+
// `TestCloneable`.
39+
return keccak256("ICloneableV2.initialize");
40+
}
41+
}

‎test/src/lib/LibICloneableFactoryV4.cloneAndInitialize.t.sol‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
33
pragma solidity =0.8.25;
44

5-
import {Test} from "forge-std-1.16.1/src/Test.sol";
5+
import {Test, Vm} from "forge-std-1.16.1/src/Test.sol";
66

77
import {ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol";
8-
import {DelegatedImplementation, InitializationFailed} from "src/lib/LibICloneableFactoryV4.sol";
8+
import {CloneAddressOccupied, DelegatedImplementation, InitializationFailed} from "src/lib/LibICloneableFactoryV4.sol";
99
import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol";
1010
import {TestCloneable} from "test/concrete/TestCloneable.sol";
11+
import {TestCloneableNestedClone} from "test/concrete/TestCloneableNestedClone.sol";
1112
import {TestCloneableRawAnswer} from "test/concrete/TestCloneableRawAnswer.sol";
1213

1314
/// @title LibICloneableFactoryV4CloneAndInitializeTest
@@ -123,4 +124,79 @@ contract LibICloneableFactoryV4CloneAndInitializeTest is Test {
123124
assertEq(I_CLONE_FACTORY.cloneDeterministicOpenSalt(implementation, data, salt), predictedOpenSalt);
124125
assertEq(predictedOpenSalt.balance, balance);
125126
}
127+
128+
/// The library holds no state, so a clone may clone through the same
129+
/// factory during its own `initialize` — an orchestrator deploying its own
130+
/// parts is the shape. Both clones land at their own derivation's address
131+
/// and are initialized with their own bytes, and the outer `NewClone` —
132+
/// emitted before `initialize` runs — precedes the nested one, whose sender
133+
/// is the outer clone.
134+
function testNestedCloneDuringInitialize(bytes32 outerSalt, bytes32 innerSalt, bytes memory innerData) external {
135+
TestCloneable innerImplementation = new TestCloneable();
136+
TestCloneableNestedClone outerImplementation = new TestCloneableNestedClone();
137+
138+
bytes memory outerData =
139+
abi.encode(address(I_CLONE_FACTORY), address(innerImplementation), innerSalt, innerData);
140+
141+
address predictedOuter =
142+
I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(outerImplementation), outerData, outerSalt);
143+
address predictedInner =
144+
I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(innerImplementation), innerData, innerSalt);
145+
146+
vm.recordLogs();
147+
address outer = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(outerImplementation), outerData, outerSalt);
148+
Vm.Log[] memory entries = vm.getRecordedLogs();
149+
150+
assertEq(outer, predictedOuter);
151+
assertEq(TestCloneableNestedClone(outer).sInner(), predictedInner);
152+
assertEq(TestCloneable(predictedInner).sData(), innerData);
153+
154+
assertEq(entries.length, 2);
155+
assertEq(entries[0].emitter, address(I_CLONE_FACTORY));
156+
assertEq(entries[0].topics[0], bytes32(uint256(keccak256("NewClone(address,address,address,bytes32,bytes)"))));
157+
assertEq(entries[0].data, abi.encode(address(this), address(outerImplementation), outer, outerSalt, outerData));
158+
assertEq(entries[1].emitter, address(I_CLONE_FACTORY));
159+
assertEq(entries[1].topics[0], bytes32(uint256(keccak256("NewClone(address,address,address,bytes32,bytes)"))));
160+
assertEq(entries[1].data, abi.encode(outer, address(innerImplementation), predictedInner, innerSalt, innerData));
161+
}
162+
163+
/// A nested clone that reverts takes the whole outer deploy with it.
164+
/// Re-entering at a salt the factory already occupies reverts
165+
/// `CloneAddressOccupied` with that address, and the outer clone is not
166+
/// left half-built: its `(deployer, salt)` is still free afterwards, so the
167+
/// same deploy at a free inner salt still lands at the address it always
168+
/// predicted. `data` is outside the namespaced derivation, which is what
169+
/// lets the retry change the inner salt and keep the outer address.
170+
function testNestedCloneAtOccupiedAddressUnwindsOuterDeploy(
171+
bytes32 outerSalt,
172+
bytes32 takenInnerSalt,
173+
bytes32 freeInnerSalt,
174+
bytes memory innerData
175+
) external {
176+
vm.assume(takenInnerSalt != freeInnerSalt);
177+
TestCloneable innerImplementation = new TestCloneable();
178+
TestCloneableNestedClone outerImplementation = new TestCloneableNestedClone();
179+
180+
address inner =
181+
I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(innerImplementation), innerData, takenInnerSalt);
182+
address predictedOuter =
183+
I_CLONE_FACTORY.predictDeterministicAddress(address(outerImplementation), outerSalt, address(this));
184+
185+
vm.expectRevert(abi.encodeWithSelector(CloneAddressOccupied.selector, inner));
186+
I_CLONE_FACTORY.cloneDeterministic(
187+
address(outerImplementation),
188+
abi.encode(address(I_CLONE_FACTORY), address(innerImplementation), takenInnerSalt, innerData),
189+
outerSalt
190+
);
191+
192+
address outer = I_CLONE_FACTORY.cloneDeterministic(
193+
address(outerImplementation),
194+
abi.encode(address(I_CLONE_FACTORY), address(innerImplementation), freeInnerSalt, innerData),
195+
outerSalt
196+
);
197+
198+
assertEq(outer, predictedOuter);
199+
assertEq(TestCloneable(TestCloneableNestedClone(outer).sInner()).sData(), innerData);
200+
assertEq(TestCloneable(inner).sData(), innerData);
201+
}
126202
}

0 commit comments

Comments
 (0)