Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/rollup/RollupCreator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@ contract RollupCreator is Ownable {
uint256 _maxFeePerGas
) internal {
if (_nativeToken == address(0)) {
// ETH already held before this call is not part of this deployment and
// must not be refunded to the caller. msg.value is what was sent for THIS
// createRollup; anything above it is pre-existing and left untouched.
uint256 preExistingBalance = address(this).balance - msg.value;

// we need to fund 4 retryable tickets
uint256 cost =
l2FactoriesDeployer.getDeploymentTotalCost(IInboxBase(_inbox), _maxFeePerGas);
Expand All @@ -323,7 +328,7 @@ contract RollupCreator is Ownable {

// refund the caller
// solhint-disable-next-line avoid-low-level-calls
(bool sent,) = msg.sender.call{value: address(this).balance}("");
(bool sent,) = msg.sender.call{value: address(this).balance - preExistingBalance}("");
require(sent, "Refund failed");
} else {
// Transfer fee token amount needed to pay for retryable fees to the inbox.
Expand Down
48 changes: 48 additions & 0 deletions test/foundry/RollupCreator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,54 @@ contract RollupCreatorTest is Test {
bytes32(uint256(keccak256("eip1967.proxy.implementation.secondary")) - 1);
return address(uint160(uint256(vm.load(proxy, secondarySlot))));
}

/// @notice Pre-existing ETH in the creator must not be swept to the caller's refund.
function test_createRollup_refundExcludesPreExistingBalance() public {
// Simulate ETH already sitting in the creator before this deployment
// (a stray transfer, dust, or leftover from a prior interaction).
uint256 stray = 0.5 ether;
vm.deal(address(rollupCreator), stray);

vm.startPrank(deployer);

Config memory config = _getDefaultConfig();

uint256 factoryDeploymentFunds = 1 ether;
vm.deal(deployer, factoryDeploymentFunds);

address[] memory batchPosters = new address[](1);
batchPosters[0] = makeAddr("batch poster 1");
address batchPosterManager = makeAddr("batch poster manager");
address[] memory validators = new address[](2);
validators[0] = makeAddr("validator1");
validators[1] = makeAddr("validator2");

RollupCreator.RollupDeploymentParams memory deployParams = RollupCreator
.RollupDeploymentParams({
config: config,
batchPosters: batchPosters,
validators: validators,
maxDataSize: MAX_DATA_SIZE,
nativeToken: address(0),
deployFactoriesToL2: true,
maxFeePerGasForRetryables: MAX_FEE_PER_GAS,
batchPosterManager: batchPosterManager,
feeTokenPricer: IFeeTokenPricer(address(0)),
customOsp: address(0)
});

rollupCreator.createRollup{value: factoryDeploymentFunds}(deployParams);

vm.stopPrank();

// The pre-existing balance must remain in the creator, not be refunded to the caller.
// Before the fix this is 0 (the whole balance was swept); after the fix it is `stray`.
assertEq(
address(rollupCreator).balance,
stray,
"pre-existing balance should stay in the creator, not be swept to the caller"
);
}
}

contract ProxyUpgradeAction {
Expand Down