Skip to content

Commit ccb29cb

Browse files
committed
feat: ID-4134: support bootstrap flow for v1 contracts first transaction.
1 parent 39abeaa commit ccb29cb

8 files changed

Lines changed: 76 additions & 30 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ scripts/*_output*.json
3030
.env.devnet
3131
.env.testnet
3232
.env.mainnet
33+
34+
lib/

scripts/deploy.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ async function main(): Promise<EnvironmentInfo> {
5252
// 4. Deploy startup wallet impl (PNR)
5353
const startupWalletImpl = await deployContractViaCREATE2(env, wallets, 'StartupWalletImpl', [walletImplLocator.address]);
5454

55-
// --- Step 4: Deployed using CREATE2 Factory.
56-
// 5. Deploy main module dynamic auth (CFC)
57-
const mainModuleDynamicAuth = await deployContractViaCREATE2(env, wallets, 'MainModuleDynamicAuth', [factory.address, startupWalletImpl.address]);
58-
59-
// --- Step 5: Deployed using Passport Nonce Reserver.
60-
// 6. Deploy immutable signer (PNR)
55+
// --- Step 4: Deployed using Passport Nonce Reserver.
56+
// 5. Deploy immutable signer (PNR)
6157
const immutableSigner = await deployContractViaCREATE2(env, wallets, 'ImmutableSigner', [signerRootAdminPubKey, signerAdminPubKey, signerAddress]);
6258

59+
// --- Step 5: Deployed using CREATE2 Factory.
60+
// 6. Deploy main module dynamic auth (CFC)
61+
const mainModuleDynamicAuth = await deployContractViaCREATE2(env, wallets, 'MainModuleDynamicAuth', [factory.address, startupWalletImpl.address, immutableSigner.address]);
62+
6363
// --- Step 6: Deployed using alternate wallet (?)
6464
// Fund the implementation changer
6565
// WARNING: If the deployment fails at this step, DO NOT RERUN without commenting out the code a prior which deploys the contracts.

scripts/step4.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import { deployContractViaCREATE2 } from './contract';
1111
async function step4(): Promise<EnvironmentInfo> {
1212
const env = loadEnvironmentInfo(hre.network.name);
1313
const { network } = env;
14-
const factoryAddress = '0x8Fa5088dF65855E0DaF87FA6591659893b24871d';
15-
const startupWalletImplAddress = '0x8FD900677aabcbB368e0a27566cCd0C7435F1926';
14+
const factoryAddress = '0x5d2F50418fB4B8a4bAd2A268Dc9DE3a5F730C4E6';
15+
const startupWalletImplAddress = '0x69aD23cB0697Bec37e12F4A970c3bF708f3b1231';
16+
const immutableSignerAddress = '0xcff469E561D9dCe5B1185CD2AC1Fa961F8fbDe61';
1617

1718
console.log(`[${network}] Starting deployment...`);
1819
console.log(`[${network}] Factory address ${factoryAddress}`);
1920
console.log(`[${network}] StartupWalletImpl address ${startupWalletImplAddress}`);
21+
console.log(`[${network}] ImmutableSigner address ${immutableSignerAddress}`);
2022

2123
await waitForInput();
2224

@@ -25,7 +27,7 @@ async function step4(): Promise<EnvironmentInfo> {
2527

2628
// --- Step 4: Deployed using CREATE2 Factory.
2729
// Deploy main module dynamic auth (CFC)
28-
const mainModuleDynamicAuth = await deployContractViaCREATE2(env, wallets, 'MainModuleDynamicAuth', [factoryAddress, startupWalletImplAddress]);
30+
const mainModuleDynamicAuth = await deployContractViaCREATE2(env, wallets, 'MainModuleDynamicAuth', [factoryAddress, startupWalletImplAddress, immutableSignerAddress]);
2931

3032
fs.writeFileSync('step4.json', JSON.stringify({
3133
factoryAddress: factoryAddress,

src/contracts/mocks/MainModuleMockV1.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import "../modules/MainModuleDynamicAuth.sol";
66

77
contract MainModuleMockV1 is MainModuleDynamicAuth {
88
// solhint-disable-next-line no-empty-blocks
9-
constructor(address _factory, address _startup) MainModuleDynamicAuth(_factory, _startup) {}
9+
constructor(address _factory, address _startupWalletImpl, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startupWalletImpl, _immutableSignerContract) {}
1010

1111

1212
}

src/contracts/mocks/MainModuleMockV2.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import "../modules/MainModuleDynamicAuth.sol";
66

77
contract MainModuleMockV2 is MainModuleDynamicAuth {
88
// solhint-disable-next-line no-empty-blocks
9-
constructor(address _factory, address _startup) MainModuleDynamicAuth(_factory, _startup) {}
9+
constructor(address _factory, address _startupWalletImpl, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startupWalletImpl, _immutableSignerContract) {}
1010

1111
function version() external pure override returns (uint256) {
1212
return 2;

src/contracts/mocks/MainModuleMockV3.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import "../modules/MainModuleDynamicAuth.sol";
66

77
contract MainModuleMockV3 is MainModuleDynamicAuth {
88
// solhint-disable-next-line no-empty-blocks
9-
constructor(address _factory, address _startup) MainModuleDynamicAuth(_factory, _startup) {}
9+
constructor(address _factory, address _startupWalletImpl, address _immutableSignerContract) MainModuleDynamicAuth(_factory, _startupWalletImpl, _immutableSignerContract) {}
1010

1111
function version() external pure override returns (uint256) {
1212
return 3;

src/contracts/modules/MainModuleDynamicAuth.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ contract MainModuleDynamicAuth is
2424
{
2525

2626
// solhint-disable-next-line no-empty-blocks
27-
constructor(address _factory, address _startup) ModuleAuthDynamic (_factory, _startup) { }
27+
constructor(address _factory, address _startupWalletImpl, address _immutableSignerContract) ModuleAuthDynamic (_factory, _startupWalletImpl, _immutableSignerContract) { }
2828

2929

3030
/**

src/contracts/modules/commons/ModuleAuthDynamic.sol

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,55 @@ import "../../Wallet.sol";
1010
abstract contract ModuleAuthDynamic is ModuleAuthUpgradable {
1111
bytes32 public immutable INIT_CODE_HASH;
1212
address public immutable FACTORY;
13+
address public immutable IMMUTABLE_SIGNER_CONTRACT;
1314

14-
constructor(address _factory, address _startupWalletImpl) {
15+
constructor(address _factory, address _startupWalletImpl, address _immutableSignerContract) {
1516
// Build init code hash of the deployed wallets using that module
1617
bytes32 initCodeHash = keccak256(abi.encodePacked(Wallet.creationCode, uint256(uint160(_startupWalletImpl))));
1718

1819
INIT_CODE_HASH = initCodeHash;
1920
FACTORY = _factory;
21+
IMMUTABLE_SIGNER_CONTRACT = _immutableSignerContract;
22+
}
23+
24+
/// @notice Calculate imageHash for Immutable-only signer
25+
/// @dev Uses the iterative hash format matching ModuleAuth._signatureValidationWithUpdateCheck
26+
/// For a single signer with threshold=1 and weight=1:
27+
/// imageHash = keccak256(abi.encode(bytes32(threshold), weight, signerAddress))
28+
/// Uses the IMMUTABLE_SIGNER_CONTRACT address directly as the signer
29+
function imageHashOfImmutableSigner() internal view returns (bytes32) {
30+
// Use the signer contract address directly (threshold=1, weight=1)
31+
bytes32 imageHash = bytes32(uint256(1)); // Start with threshold
32+
imageHash = keccak256(abi.encode(imageHash, uint256(1), IMMUTABLE_SIGNER_CONTRACT)); // Apply weight=1, contract address
33+
return imageHash;
2034
}
2135

2236
/**
23-
* @notice Validates the signature image with the salt used to deploy the contract
24-
* if there is no stored image hash. This will happen prior to the first meta
25-
* transaction. Subsequently, validate the
26-
* signature image with a valid image hash defined in the contract storage
27-
* @param _imageHash Hash image of signature
28-
* @return true if the signature image is valid, and true if the image hash needs to be updated
37+
* @notice Validates the given signature image hash against the known valid patterns,
38+
* supporting both normal and bootstrap/upgrade paths:
39+
* - If there is no stored image hash (first transaction after deployment),
40+
* allows authentication in two ways:
41+
* 1. If the image hash was used as the salt for counterfactual wallet deployment,
42+
* the image is valid and should now be stored.
43+
* 2. Alternatively, if the image hash matches the hash derived from the
44+
* Immutable Signer contract, the image is also valid and should be stored.
45+
* - In all these initial cases, the return value requests that the image hash
46+
* is recorded for future use (second return value is true).
47+
* - If a stored image hash exists, only that exact image hash is considered valid.
48+
* In this case, there is no need to update the stored image hash
49+
* (second return value is false).
50+
* @param _imageHash Hash image of the signature
51+
* @return (bool, bool) First value true if the image hash is valid,
52+
* second value true if the image hash needs to be stored/updated.
2953
*/
3054
function _isValidImage(bytes32 _imageHash) internal view override returns (bool, bool) {
55+
// Standard validation: Check if CFA matches (for normal deployment)
3156
bytes32 storedImageHash = ModuleStorage.readBytes32(ImageHashKey.IMAGE_HASH_KEY);
57+
3258
if (storedImageHash == 0) {
3359
// No image hash stored. Check that the image hash was used as the salt when
3460
// deploying the wallet proxy contract.
35-
bool authenticated = address(
61+
address computedAddress = address(
3662
uint160(uint256(
3763
keccak256(
3864
abi.encodePacked(
@@ -43,15 +69,31 @@ abstract contract ModuleAuthDynamic is ModuleAuthUpgradable {
4369
)
4470
)
4571
))
46-
) == address(this);
72+
);
73+
74+
bool authenticated = computedAddress == address(this);
75+
4776
// Indicate need to update = true. This will trigger a call to store the image hash
48-
return (authenticated, true);
49-
}
77+
if (authenticated) {
78+
return (true, true);
79+
} else {
80+
// BOOTSTRAP MODE: Check if signed by Immutable signer only
81+
// This allows deploying a wallet with a different salt (from another chain)
82+
// and using Immutable-only signature to authorize the first transaction
83+
bytes32 immutableImageHash = imageHashOfImmutableSigner();
84+
85+
if (_imageHash == immutableImageHash) {
86+
return (true, true); // Bootstrap with immutable signer
87+
}
88+
}
5089

51-
// Image hash has been stored.
52-
return ((_imageHash != bytes32(0) && _imageHash == storedImageHash), false);
90+
return (false, false); // Invalid signature
91+
}
92+
93+
// Image hash has been stored. Compare it with the provided image hash
94+
bool isValid = _imageHash != bytes32(0) && _imageHash == storedImageHash;
95+
96+
// Return the result of the comparison. No need to update the image hash.
97+
return (isValid, false);
5398
}
54-
}
55-
56-
57-
99+
}

0 commit comments

Comments
 (0)