Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/lib/LibRainDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ library LibRainDeploy {
/// Thrown when no networks are provided for deployment.
error NoNetworks();

/// Thrown when attempting to find the deploy block of a contract that has
/// no code at the current block.
error NotDeployed(address target);

/// Thrown when the target already has code at the start block, meaning
/// the deploy may have happened before the search range.
error DeployedBeforeStartBlock(address target, uint256 startBlock);

/// Zoltu factory is the same on every network.
address constant ZOLTU_FACTORY = 0x7A0D94F55792C434d74a40883C6ed8545E406D12;

Expand All @@ -56,6 +64,82 @@ library LibRainDeploy {
/// Config name for Polygon network.
string constant POLYGON = "polygon";

/// Checks whether a block is the first block where a contract with the
/// expected code hash exists. True when the target has the expected code
/// hash at `blockNumber` and does NOT have it at `blockNumber - 1`. At
/// block 0, only the first condition is checked. The fork is restored to
/// its original block number after checking.
/// @param vm The Vm instance for fork manipulation.
/// @param target The contract address to check.
/// @param expectedCodeHash The code hash to look for.
/// @param blockNumber The block number to check.
/// @return isStart True if the contract first appears at this block.
function isStartBlock(Vm vm, address target, bytes32 expectedCodeHash, uint256 blockNumber)
internal
returns (bool isStart)
{
uint256 originalBlock = block.number;
vm.rollFork(blockNumber);
isStart = target.codehash == expectedCodeHash;
if (isStart && blockNumber > 0) {
vm.rollFork(blockNumber - 1);
isStart = target.codehash != expectedCodeHash;
}
vm.rollFork(originalBlock);
}

/// Finds the block number at which a contract was first deployed by binary
/// searching the fork history. Requires an active fork with archive access
/// back to `startBlock`. The fork is restored to its original block
/// number before returning. The target's code hash is verified against the
/// expected value before searching. The result is validated via
/// `isStartBlock`.
Comment on lines +91 to +96
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation states validation that isn't performed.

The docstring says "The result is validated via isStartBlock" but the implementation doesn't call isStartBlock. The tests call it separately for verification (see testFindDeployBlockZoltuFactory). Either add the validation call or update the docstring to clarify that callers should validate the result.

📝 Suggested docstring fix
 /// Finds the block number at which a contract was first deployed by binary
 /// searching the fork history. Requires an active fork with archive access
 /// back to `startBlock`. The fork is restored to its original block
 /// number before returning. The target's code hash is verified against the
-/// expected value before searching. The result is validated via
-/// `isStartBlock`.
+/// expected value before searching. Callers can validate the result via
+/// `isStartBlock` if needed.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Finds the block number at which a contract was first deployed by binary
/// searching the fork history. Requires an active fork with archive access
/// back to `startBlock`. The fork is restored to its original block
/// number before returning. The target's code hash is verified against the
/// expected value before searching. The result is validated via
/// `isStartBlock`.
/// Finds the block number at which a contract was first deployed by binary
/// searching the fork history. Requires an active fork with archive access
/// back to `startBlock`. The fork is restored to its original block
/// number before returning. The target's code hash is verified against the
/// expected value before searching. Callers can validate the result via
/// `isStartBlock` if needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/LibRainDeploy.sol` around lines 91 - 96, The docstring for the
deployment search claims the result "is validated via `isStartBlock`" but the
function `findDeployBlock` doesn't call `isStartBlock`; update `findDeployBlock`
to invoke `isStartBlock(foundBlock, target)` (or the appropriate signature)
after locating the candidate block and before returning, and revert or return an
error if `isStartBlock` returns false; ensure the fork is still restored to its
original block number before reverting/returning.

/// @param vm The Vm instance for fork manipulation.
/// @param target The contract address to search for.
/// @param expectedCodeHash The expected code hash of the target contract.
/// @param startBlock The earliest block to search from. The target MUST
/// NOT have the expected code hash at this block.
/// @return deployBlock The first block number where `target` has the
/// expected code hash.
function findDeployBlock(Vm vm, address target, bytes32 expectedCodeHash, uint256 startBlock)
internal
returns (uint256 deployBlock)
{
if (target.code.length == 0) {
revert NotDeployed(target);
}
if (target.codehash != expectedCodeHash) {
revert UnexpectedDeployedCodeHash(expectedCodeHash, target.codehash);
}

uint256 originalBlock = block.number;

// Verify the target does not already have the expected code at
// startBlock. If it does, the deploy happened before our search
// range and the result would be meaningless.
vm.rollFork(startBlock);
if (target.codehash == expectedCodeHash) {
vm.rollFork(originalBlock);
revert DeployedBeforeStartBlock(target, startBlock);
}

uint256 low = startBlock;
uint256 high = originalBlock;

while (low < high) {
uint256 mid = (low + high) / 2;
vm.rollFork(mid);
if (target.codehash == expectedCodeHash) {
high = mid;
} else {
low = mid + 1;
}
}

deployBlock = low;
vm.rollFork(originalBlock);
}

/// Etches the Zoltu factory bytecode into the factory address. Useful for
/// networks where the factory is not yet deployed.
/// @param vm The Vm instance to use for etching.
Expand Down
125 changes: 125 additions & 0 deletions test/src/lib/LibRainDeploy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,131 @@ contract MockReverter {
contract LibRainDeployTest is Test {
mapping(string => mapping(address => bytes32)) internal sDepCodeHashes;

/// External wrapper for `isStartBlock` so that it can be called
/// externally in tests.
/// @param target The contract address to check.
/// @param expectedCodeHash The code hash to look for.
/// @param blockNumber The block number to check.
/// @return isStart True if the contract first appears at this block.
function externalIsStartBlock(address target, bytes32 expectedCodeHash, uint256 blockNumber)
external
returns (bool isStart)
{
isStart = LibRainDeploy.isStartBlock(vm, target, expectedCodeHash, blockNumber);
}

/// External wrapper for `findDeployBlock` so that `vm.expectRevert`
/// works at the correct call depth.
/// @param target The contract address to search for.
/// @param expectedCodeHash The expected code hash of the target.
/// @param startBlock The earliest block to search from.
/// @return deployBlock The first block number where `target` has code.
function externalFindDeployBlock(address target, bytes32 expectedCodeHash, uint256 startBlock)
external
returns (uint256 deployBlock)
{
deployBlock = LibRainDeploy.findDeployBlock(vm, target, expectedCodeHash, startBlock);
}

/// `isStartBlock` MUST return false when the target has no code at the
/// given block.
function testIsStartBlockNoCode() external {
vm.createSelectFork(LibRainDeploy.BASE);
assertFalse(LibRainDeploy.isStartBlock(vm, address(0xdead), bytes32(uint256(1)), block.number));
}

/// `isStartBlock` MUST return false when the target has the expected
/// code hash at both the given block and the block before it.
function testIsStartBlockCodeAtBothBlocks() external {
vm.createSelectFork(LibRainDeploy.BASE);
// The Zoltu factory exists at the current block and the block
// before it, so this is not a start block.
assertFalse(
LibRainDeploy.isStartBlock(
vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, block.number
)
);
}

/// `isStartBlock` MUST return true when the target has the expected code
/// hash at the given block but not at the block before it. Uses the
/// actual Zoltu factory deploy block found by `findDeployBlock`.
function testIsStartBlockAtDeployBlock() external {
vm.createSelectFork(LibRainDeploy.BASE);
uint256 deployBlock =
LibRainDeploy.findDeployBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0);
assertTrue(
LibRainDeploy.isStartBlock(
vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, deployBlock
)
);
}

/// `isStartBlock` MUST restore the fork to its original block number.
function testIsStartBlockRestoresFork() external {
vm.createSelectFork(LibRainDeploy.BASE);
uint256 originalBlock = block.number;
LibRainDeploy.isStartBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0);
assertEq(block.number, originalBlock);
}

/// `findDeployBlock` MUST revert with `NotDeployed` when the target
/// address has no code on the current fork.
function testFindDeployBlockNotDeployedReverts() external {
vm.createSelectFork(LibRainDeploy.BASE);
vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.NotDeployed.selector, address(0xdead)));
this.externalFindDeployBlock(address(0xdead), bytes32(0), 0);
}

/// `findDeployBlock` MUST revert with `UnexpectedDeployedCodeHash` when
/// the target's code hash does not match the expected value.
function testFindDeployBlockWrongCodeHashReverts() external {
vm.createSelectFork(LibRainDeploy.BASE);
bytes32 wrongHash = bytes32(uint256(1));
vm.expectRevert(
abi.encodeWithSelector(
LibRainDeploy.UnexpectedDeployedCodeHash.selector, wrongHash, LibRainDeploy.ZOLTU_FACTORY_CODEHASH
)
);
this.externalFindDeployBlock(LibRainDeploy.ZOLTU_FACTORY, wrongHash, 0);
}

/// `findDeployBlock` MUST revert with `DeployedBeforeStartBlock` when
/// the target already has code at the start block.
function testFindDeployBlockDeployedBeforeStartBlockReverts() external {
vm.createSelectFork(LibRainDeploy.BASE);
// Use the current block as startBlock — the Zoltu factory already
// exists here, so the function should revert.
uint256 startBlock = block.number;
vm.expectRevert(
abi.encodeWithSelector(
LibRainDeploy.DeployedBeforeStartBlock.selector, LibRainDeploy.ZOLTU_FACTORY, startBlock
)
);
this.externalFindDeployBlock(LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, startBlock);
}

/// `findDeployBlock` MUST return a block that `isStartBlock` confirms,
/// and the fork MUST be restored to the original block number.
/// Uses Base because the public Base RPC has full archive access.
function testFindDeployBlockZoltuFactory() external {
vm.createSelectFork(LibRainDeploy.BASE);
uint256 originalBlock = block.number;

uint256 deployBlock =
LibRainDeploy.findDeployBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0);

// Fork must be restored to the original block.
assertEq(block.number, originalBlock);

// The result must be a valid start block.
assertTrue(
LibRainDeploy.isStartBlock(
vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, deployBlock
)
);
}

/// `supportedNetworks` MUST return exactly 5 networks in the expected
/// order matching the library constants.
function testSupportedNetworks() external pure {
Expand Down
Loading