Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ scripts/*_output*.json
.env.devnet
.env.testnet
.env.mainnet

lib/
12 changes: 6 additions & 6 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ async function main(): Promise<EnvironmentInfo> {
// 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.
Expand Down
8 changes: 5 additions & 3 deletions scripts/step4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { deployContractViaCREATE2 } from './contract';
async function step4(): Promise<EnvironmentInfo> {
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();

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

// --- 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,
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/mocks/MainModuleMockV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}


}
2 changes: 1 addition & 1 deletion src/contracts/mocks/MainModuleMockV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/mocks/MainModuleMockV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/modules/MainModuleDynamicAuth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }


/**
Expand Down
76 changes: 59 additions & 17 deletions src/contracts/modules/commons/ModuleAuthDynamic.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
}
}



}
Loading