From bd6f04f0f9b2d5ed913bc4a2da55cdbcc586a25e Mon Sep 17 00:00:00 2001 From: Natalie Bunduwongse Date: Mon, 17 Nov 2025 18:19:52 +1300 Subject: [PATCH] feat: bootstrap logic --- src/contracts/mocks/MainModuleMockV1.sol | 2 +- src/contracts/mocks/MainModuleMockV2.sol | 2 +- src/contracts/mocks/MainModuleMockV3.sol | 2 +- .../modules/MainModuleDynamicAuth.sol | 6 ++- .../modules/commons/ModuleAuthDynamic.sol | 54 +++++++++++++++++-- 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/contracts/mocks/MainModuleMockV1.sol b/src/contracts/mocks/MainModuleMockV1.sol index 47807abc..e5f77a34 100644 --- a/src/contracts/mocks/MainModuleMockV1.sol +++ b/src/contracts/mocks/MainModuleMockV1.sol @@ -6,7 +6,7 @@ import "../modules/MainModuleDynamicAuth.sol"; contract MainModuleMockV1 is MainModuleDynamicAuth { // solhint-disable-next-line no-empty-blocks - constructor(address _factory, address _startup) MainModuleDynamicAuth(_factory, _startup) {} + constructor(address _factory, address _startup, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startup, _immutableSignerContract) {} } diff --git a/src/contracts/mocks/MainModuleMockV2.sol b/src/contracts/mocks/MainModuleMockV2.sol index d0ed7087..b2daff6b 100644 --- a/src/contracts/mocks/MainModuleMockV2.sol +++ b/src/contracts/mocks/MainModuleMockV2.sol @@ -6,7 +6,7 @@ import "../modules/MainModuleDynamicAuth.sol"; contract MainModuleMockV2 is MainModuleDynamicAuth { // solhint-disable-next-line no-empty-blocks - constructor(address _factory, address _startup) MainModuleDynamicAuth(_factory, _startup) {} + constructor(address _factory, address _startup, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startup, _immutableSignerContract) {} function version() external pure override returns (uint256) { return 2; diff --git a/src/contracts/mocks/MainModuleMockV3.sol b/src/contracts/mocks/MainModuleMockV3.sol index 1f590a7c..5ca6dc14 100644 --- a/src/contracts/mocks/MainModuleMockV3.sol +++ b/src/contracts/mocks/MainModuleMockV3.sol @@ -6,7 +6,7 @@ import "../modules/MainModuleDynamicAuth.sol"; contract MainModuleMockV3 is MainModuleDynamicAuth { // solhint-disable-next-line no-empty-blocks - constructor(address _factory, address _startup) MainModuleDynamicAuth(_factory, _startup) {} + constructor(address _factory, address _startup, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startup, _immutableSignerContract) {} function version() external pure override returns (uint256) { return 3; diff --git a/src/contracts/modules/MainModuleDynamicAuth.sol b/src/contracts/modules/MainModuleDynamicAuth.sol index 149ce806..1436cdca 100644 --- a/src/contracts/modules/MainModuleDynamicAuth.sol +++ b/src/contracts/modules/MainModuleDynamicAuth.sol @@ -24,7 +24,11 @@ contract MainModuleDynamicAuth is { // solhint-disable-next-line no-empty-blocks - constructor(address _factory, address _startup) ModuleAuthDynamic (_factory, _startup) { } + constructor( + address _factory, + address _startup, + address _immutableSignerContract + ) ModuleAuthDynamic (_factory, _startup, _immutableSignerContract) { } /** diff --git a/src/contracts/modules/commons/ModuleAuthDynamic.sol b/src/contracts/modules/commons/ModuleAuthDynamic.sol index 505c4ed8..73389e92 100644 --- a/src/contracts/modules/commons/ModuleAuthDynamic.sol +++ b/src/contracts/modules/commons/ModuleAuthDynamic.sol @@ -6,17 +6,28 @@ import "./ModuleAuthUpgradable.sol"; import "./ImageHashKey.sol"; import "../../Wallet.sol"; +interface IImmutableSigner { + struct ExpirableSigner { + address signer; + uint256 validUntil; + } + + function primarySigner() external view returns (address); + function rolloverSigner() external view returns (ExpirableSigner memory); +} abstract contract ModuleAuthDynamic is ModuleAuthUpgradable { bytes32 public immutable INIT_CODE_HASH; address public immutable FACTORY; + address public immutable IMMUTABLE_SIGNER_CONTRACT; - constructor(address _factory, address _startupWalletImpl) { + constructor(address _factory, address _startupWalletImpl, address _immutableSignerContract) { // Build init code hash of the deployed wallets using that module bytes32 initCodeHash = keccak256(abi.encodePacked(Wallet.creationCode, uint256(uint160(_startupWalletImpl)))); INIT_CODE_HASH = initCodeHash; FACTORY = _factory; + IMMUTABLE_SIGNER_CONTRACT = _immutableSignerContract; } /** @@ -30,8 +41,45 @@ abstract contract ModuleAuthDynamic is ModuleAuthUpgradable { function _isValidImage(bytes32 _imageHash) internal view override returns (bool, bool) { bytes32 storedImageHash = ModuleStorage.readBytes32(ImageHashKey.IMAGE_HASH_KEY); if (storedImageHash == 0) { - // No image hash stored. Check that the image hash was used as the salt when - // deploying the wallet proxy contract. + // BOOTSTRAP MODE: Check if signed by Immutable signer only (for cross-chain deployment) + // This allows deploying a wallet on a new chain with the same address but different signers + // by temporarily accepting Immutable-only signature, then updating to the new signer set + + // Check PRIMARY signer + address primarySignerEOA = IImmutableSigner(IMMUTABLE_SIGNER_CONTRACT).primarySigner(); + bytes32 primaryImageHash = keccak256( + abi.encode( + bytes32(uint256(1)), + uint8(1), + primarySignerEOA + ) + ); + + if (_imageHash == primaryImageHash) { + // Valid bootstrap signature with primary signer - accept and store + return (true, true); + } + + // Check ROLLOVER signer (if still valid) + IImmutableSigner.ExpirableSigner memory rollover = IImmutableSigner(IMMUTABLE_SIGNER_CONTRACT).rolloverSigner(); + if (block.timestamp <= rollover.validUntil && rollover.signer != address(0)) { + bytes32 rolloverImageHash = keccak256( + abi.encode( + bytes32(uint256(1)), + uint8(1), + rollover.signer + ) + ); + + if (_imageHash == rolloverImageHash) { + // Valid bootstrap signature with rollover signer - accept and store + return (true, true); + } + } + + // NORMAL MODE: Check that the image hash was used as the salt when + // deploying the wallet proxy contract (for first deployment on original chain) + // Backwards compatibility with previous deployment logic. bool authenticated = address( uint160(uint256( keccak256(