From ccb29cb4b87fe04a05f6446285d070350bc79d74 Mon Sep 17 00:00:00 2001 From: Naveen <116692862+naveen-imtb@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:20:29 +1100 Subject: [PATCH] feat: ID-4134: support bootstrap flow for v1 contracts first transaction. --- .gitignore | 2 + scripts/deploy.ts | 12 +-- scripts/step4.ts | 8 +- src/contracts/mocks/MainModuleMockV1.sol | 2 +- src/contracts/mocks/MainModuleMockV2.sol | 2 +- src/contracts/mocks/MainModuleMockV3.sol | 2 +- .../modules/MainModuleDynamicAuth.sol | 2 +- .../modules/commons/ModuleAuthDynamic.sol | 76 ++++++++++++++----- 8 files changed, 76 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 5c36a662..e4fdf0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ scripts/*_output*.json .env.devnet .env.testnet .env.mainnet + +lib/ diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 7b2d9f99..517d1ce3 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -52,14 +52,14 @@ async function main(): Promise { // 4. Deploy startup wallet impl (PNR) const startupWalletImpl = await deployContractViaCREATE2(env, wallets, 'StartupWalletImpl', [walletImplLocator.address]); - // --- Step 4: Deployed using CREATE2 Factory. - // 5. Deploy main module dynamic auth (CFC) - const mainModuleDynamicAuth = await deployContractViaCREATE2(env, wallets, 'MainModuleDynamicAuth', [factory.address, startupWalletImpl.address]); - - // --- Step 5: Deployed using Passport Nonce Reserver. - // 6. Deploy immutable signer (PNR) + // --- Step 4: Deployed using Passport Nonce Reserver. + // 5. Deploy immutable signer (PNR) const immutableSigner = await deployContractViaCREATE2(env, wallets, 'ImmutableSigner', [signerRootAdminPubKey, signerAdminPubKey, signerAddress]); + // --- Step 5: Deployed using CREATE2 Factory. + // 6. Deploy main module dynamic auth (CFC) + const mainModuleDynamicAuth = await deployContractViaCREATE2(env, wallets, 'MainModuleDynamicAuth', [factory.address, startupWalletImpl.address, immutableSigner.address]); + // --- Step 6: Deployed using alternate wallet (?) // Fund the implementation changer // WARNING: If the deployment fails at this step, DO NOT RERUN without commenting out the code a prior which deploys the contracts. diff --git a/scripts/step4.ts b/scripts/step4.ts index b7d3f031..69163a22 100644 --- a/scripts/step4.ts +++ b/scripts/step4.ts @@ -11,12 +11,14 @@ import { deployContractViaCREATE2 } from './contract'; async function step4(): Promise { const env = loadEnvironmentInfo(hre.network.name); const { network } = env; - const factoryAddress = '0x8Fa5088dF65855E0DaF87FA6591659893b24871d'; - const startupWalletImplAddress = '0x8FD900677aabcbB368e0a27566cCd0C7435F1926'; + const factoryAddress = '0x5d2F50418fB4B8a4bAd2A268Dc9DE3a5F730C4E6'; + const startupWalletImplAddress = '0x69aD23cB0697Bec37e12F4A970c3bF708f3b1231'; + const immutableSignerAddress = '0xcff469E561D9dCe5B1185CD2AC1Fa961F8fbDe61'; console.log(`[${network}] Starting deployment...`); console.log(`[${network}] Factory address ${factoryAddress}`); console.log(`[${network}] StartupWalletImpl address ${startupWalletImplAddress}`); + console.log(`[${network}] ImmutableSigner address ${immutableSignerAddress}`); await waitForInput(); @@ -25,7 +27,7 @@ async function step4(): Promise { // --- Step 4: Deployed using CREATE2 Factory. // Deploy main module dynamic auth (CFC) - const mainModuleDynamicAuth = await deployContractViaCREATE2(env, wallets, 'MainModuleDynamicAuth', [factoryAddress, startupWalletImplAddress]); + const mainModuleDynamicAuth = await deployContractViaCREATE2(env, wallets, 'MainModuleDynamicAuth', [factoryAddress, startupWalletImplAddress, immutableSignerAddress]); fs.writeFileSync('step4.json', JSON.stringify({ factoryAddress: factoryAddress, diff --git a/src/contracts/mocks/MainModuleMockV1.sol b/src/contracts/mocks/MainModuleMockV1.sol index 47807abc..6c1a6b23 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 _startupWalletImpl, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startupWalletImpl, _immutableSignerContract) {} } diff --git a/src/contracts/mocks/MainModuleMockV2.sol b/src/contracts/mocks/MainModuleMockV2.sol index d0ed7087..a7a33973 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 _startupWalletImpl, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startupWalletImpl, _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..88f7087f 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 _startupWalletImpl, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startupWalletImpl, _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..ccaec783 100644 --- a/src/contracts/modules/MainModuleDynamicAuth.sol +++ b/src/contracts/modules/MainModuleDynamicAuth.sol @@ -24,7 +24,7 @@ contract MainModuleDynamicAuth is { // solhint-disable-next-line no-empty-blocks - constructor(address _factory, address _startup) ModuleAuthDynamic (_factory, _startup) { } + constructor(address _factory, address _startupWalletImpl, address _immutableSignerContract) ModuleAuthDynamic (_factory, _startupWalletImpl, _immutableSignerContract) { } /** diff --git a/src/contracts/modules/commons/ModuleAuthDynamic.sol b/src/contracts/modules/commons/ModuleAuthDynamic.sol index 505c4ed8..ccd8b16a 100644 --- a/src/contracts/modules/commons/ModuleAuthDynamic.sol +++ b/src/contracts/modules/commons/ModuleAuthDynamic.sol @@ -10,29 +10,55 @@ import "../../Wallet.sol"; 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; + } + + /// @notice Calculate imageHash for Immutable-only signer + /// @dev Uses the iterative hash format matching ModuleAuth._signatureValidationWithUpdateCheck + /// For a single signer with threshold=1 and weight=1: + /// imageHash = keccak256(abi.encode(bytes32(threshold), weight, signerAddress)) + /// Uses the IMMUTABLE_SIGNER_CONTRACT address directly as the signer + function imageHashOfImmutableSigner() internal view returns (bytes32) { + // Use the signer contract address directly (threshold=1, weight=1) + bytes32 imageHash = bytes32(uint256(1)); // Start with threshold + imageHash = keccak256(abi.encode(imageHash, uint256(1), IMMUTABLE_SIGNER_CONTRACT)); // Apply weight=1, contract address + return imageHash; } /** - * @notice Validates the signature image with the salt used to deploy the contract - * if there is no stored image hash. This will happen prior to the first meta - * transaction. Subsequently, validate the - * signature image with a valid image hash defined in the contract storage - * @param _imageHash Hash image of signature - * @return true if the signature image is valid, and true if the image hash needs to be updated + * @notice Validates the given signature image hash against the known valid patterns, + * supporting both normal and bootstrap/upgrade paths: + * - If there is no stored image hash (first transaction after deployment), + * allows authentication in two ways: + * 1. If the image hash was used as the salt for counterfactual wallet deployment, + * the image is valid and should now be stored. + * 2. Alternatively, if the image hash matches the hash derived from the + * Immutable Signer contract, the image is also valid and should be stored. + * - In all these initial cases, the return value requests that the image hash + * is recorded for future use (second return value is true). + * - If a stored image hash exists, only that exact image hash is considered valid. + * In this case, there is no need to update the stored image hash + * (second return value is false). + * @param _imageHash Hash image of the signature + * @return (bool, bool) First value true if the image hash is valid, + * second value true if the image hash needs to be stored/updated. */ function _isValidImage(bytes32 _imageHash) internal view override returns (bool, bool) { + // Standard validation: Check if CFA matches (for normal deployment) 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. - bool authenticated = address( + address computedAddress = address( uint160(uint256( keccak256( abi.encodePacked( @@ -43,15 +69,31 @@ abstract contract ModuleAuthDynamic is ModuleAuthUpgradable { ) ) )) - ) == address(this); + ); + + bool authenticated = computedAddress == address(this); + // Indicate need to update = true. This will trigger a call to store the image hash - return (authenticated, true); - } + if (authenticated) { + return (true, true); + } else { + // BOOTSTRAP MODE: Check if signed by Immutable signer only + // This allows deploying a wallet with a different salt (from another chain) + // and using Immutable-only signature to authorize the first transaction + bytes32 immutableImageHash = imageHashOfImmutableSigner(); + + if (_imageHash == immutableImageHash) { + return (true, true); // Bootstrap with immutable signer + } + } - // Image hash has been stored. - return ((_imageHash != bytes32(0) && _imageHash == storedImageHash), false); + return (false, false); // Invalid signature + } + + // Image hash has been stored. Compare it with the provided image hash + bool isValid = _imageHash != bytes32(0) && _imageHash == storedImageHash; + + // Return the result of the comparison. No need to update the image hash. + return (isValid, false); } -} - - - +} \ No newline at end of file