Skip to content

Commit 7716f65

Browse files
committed
chore: ID-4134: bootstrap wallet for initial txn
Hook into signature validation to bootstrap wallet deployment for first txn without user signature.
1 parent 39abeaa commit 7716f65

File tree

10 files changed

+169
-26
lines changed

10 files changed

+169
-26
lines changed

.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/

hardhat.config.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,19 @@ loadAndValidateEnvironment();
1616

1717
const config: HardhatUserConfig = {
1818
solidity: {
19-
compilers: [{ version: '0.8.17' }],
20-
settings: {
21-
optimizer: {
22-
enabled: true,
23-
runs: 999999,
24-
details: {
25-
yul: true
19+
compilers: [{
20+
version: '0.8.17',
21+
settings: {
22+
viaIR: true,
23+
optimizer: {
24+
enabled: true,
25+
runs: 999999,
26+
details: {
27+
yul: true
28+
}
2629
}
2730
}
28-
}
31+
}]
2932
},
3033
paths: {
3134
root: 'src',

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ async function step4(): Promise<EnvironmentInfo> {
1313
const { network } = env;
1414
const factoryAddress = '0x8Fa5088dF65855E0DaF87FA6591659893b24871d';
1515
const startupWalletImplAddress = '0x8FD900677aabcbB368e0a27566cCd0C7435F1926';
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: 2 additions & 2 deletions
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

12-
}
12+
}

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/ModuleAuth.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import "./ModuleERC165.sol";
1313
abstract contract ModuleAuth is IModuleAuth, ModuleERC165, SignatureValidator, IERC1271Wallet {
1414
using LibBytes for bytes;
1515

16-
uint256 private constant FLAG_SIGNATURE = 0;
17-
uint256 private constant FLAG_ADDRESS = 1;
18-
uint256 private constant FLAG_DYNAMIC_SIGNATURE = 2;
16+
uint256 internal constant FLAG_SIGNATURE = 0;
17+
uint256 internal constant FLAG_ADDRESS = 1;
18+
uint256 internal constant FLAG_DYNAMIC_SIGNATURE = 2;
1919

2020
bytes4 private constant SELECTOR_ERC1271_BYTES_BYTES = 0x20c13b0b;
2121
bytes4 private constant SELECTOR_ERC1271_BYTES32_BYTES = 0x1626ba7e;
@@ -49,7 +49,7 @@ abstract contract ModuleAuth is IModuleAuth, ModuleERC165, SignatureValidator, I
4949
bytes32 _hash,
5050
bytes memory _signature
5151
)
52-
internal override returns (bool)
52+
internal virtual override returns (bool)
5353
{
5454
(bool verified, bool needsUpdate, bytes32 imageHash) = _signatureValidationWithUpdateCheck(_hash, _signature);
5555
if (needsUpdate) {
@@ -74,7 +74,7 @@ abstract contract ModuleAuth is IModuleAuth, ModuleERC165, SignatureValidator, I
7474
bytes32 _hash,
7575
bytes memory _signature
7676
)
77-
internal view returns (bool, bool, bytes32)
77+
internal view virtual returns (bool, bool, bytes32)
7878
{
7979
(
8080
uint16 threshold, // required threshold signature

src/contracts/modules/commons/ModuleAuthDynamic.sol

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,155 @@ pragma solidity 0.8.17;
44

55
import "./ModuleAuthUpgradable.sol";
66
import "./ImageHashKey.sol";
7+
import "./ModuleStorage.sol";
8+
import "./NonceKey.sol";
79
import "../../Wallet.sol";
810

11+
import "../../utils/LibBytes.sol";
912

1013
abstract contract ModuleAuthDynamic is ModuleAuthUpgradable {
14+
using LibBytes for bytes;
15+
1116
bytes32 public immutable INIT_CODE_HASH;
1217
address public immutable FACTORY;
18+
address public immutable IMMUTABLE_SIGNER_CONTRACT;
1319

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

1824
INIT_CODE_HASH = initCodeHash;
1925
FACTORY = _factory;
26+
IMMUTABLE_SIGNER_CONTRACT = _immutableSignerContract;
27+
}
28+
29+
/**
30+
* @notice Verify if signer is default wallet owner
31+
* @param _hash Hashed signed message
32+
* @param _signature Array of signatures with signers ordered
33+
* like the the keys in the multisig configs
34+
*
35+
* @dev The signature must be solidity packed and contain the total number of owners,
36+
* the threshold, the weight and either the address or a signature for each owner.
37+
*
38+
* Each weight & (address or signature) pair is prefixed by a flag that signals if such pair
39+
* contains an address or a signature. The aggregated weight of the signatures must surpass the threshold.
40+
*
41+
* Flag types:
42+
* 0x00 - Signature
43+
* 0x01 - Address
44+
*
45+
* E.g:
46+
* abi.encodePacked(
47+
* uint16 threshold,
48+
* uint8 01, uint8 weight_1, address signer_1,
49+
* uint8 00, uint8 weight_2, bytes signature_2,
50+
* ...
51+
* uint8 01, uint8 weight_5, address signer_5
52+
* )
53+
*/
54+
function _signatureValidation(
55+
bytes32 _hash,
56+
bytes memory _signature
57+
)
58+
internal virtual override returns (bool)
59+
{
60+
(bool verified, bool needsUpdate, bytes32 imageHash) = _signatureValidationWithUpdateCheck(_hash, _signature);
61+
if (needsUpdate) {
62+
updateImageHashInternal(imageHash);
63+
}
64+
return verified;
65+
}
66+
67+
/**
68+
* @notice Verify signature and determine if image hash needs updating
69+
* @param _hash Hashed signed message
70+
* @param _signature Packed signature data containing threshold, flags, weights, and addresses/signatures
71+
* @return verified True if the signature is valid and weight threshold is met
72+
* @return needsUpdate True if the image hash needs to be stored (first transaction)
73+
* @return imageHash The computed image hash from the signature
74+
*
75+
* @dev This function parses the signature, recovers/reads signer addresses, and validates them.
76+
* For defensive validation, each extracted address is compared against IMMUTABLE_SIGNER_CONTRACT.
77+
* If a match is found, it is recorded.
78+
*
79+
* Special case: If this is the first transaction (nonce == 0) and the immutable signer contract
80+
* is one of the signers, the signature is automatically validated and approved without checking
81+
* the stored image hash. This allows the immutable signer to bootstrap the wallet on first use.
82+
*/
83+
function _signatureValidationWithUpdateCheck(
84+
bytes32 _hash,
85+
bytes memory _signature
86+
)
87+
internal view override returns (bool, bool, bytes32)
88+
{
89+
(
90+
uint16 threshold, // required threshold signature
91+
uint256 rindex // read index
92+
) = _signature.readFirstUint16();
93+
94+
// Start image hash generation
95+
bytes32 imageHash = bytes32(uint256(threshold));
96+
97+
// Acumulated weight of signatures
98+
uint256 totalWeight;
99+
100+
// Track if immutable signer contract is one of the signers
101+
bool immutableSignerContractFound = false;
102+
103+
// Iterate until the image is completed
104+
while (rindex < _signature.length) {
105+
// Read next item type and addrWeight
106+
uint256 flag; uint256 addrWeight; address addr;
107+
(flag, addrWeight, rindex) = _signature.readUint8Uint8(rindex);
108+
109+
if (flag == FLAG_ADDRESS) {
110+
// Read plain address
111+
(addr, rindex) = _signature.readAddress(rindex);
112+
} else if (flag == FLAG_SIGNATURE) {
113+
// Read single signature and recover signer
114+
bytes memory signature;
115+
(signature, rindex) = _signature.readBytes66(rindex);
116+
addr = recoverSigner(_hash, signature);
117+
118+
// Acumulate total weight of the signature
119+
totalWeight += addrWeight;
120+
} else if (flag == FLAG_DYNAMIC_SIGNATURE) {
121+
// Read signer
122+
(addr, rindex) = _signature.readAddress(rindex);
123+
124+
// Read signature size
125+
uint256 size;
126+
(size, rindex) = _signature.readUint16(rindex);
127+
128+
// Read dynamic size signature
129+
bytes memory signature;
130+
(signature, rindex) = _signature.readBytes(rindex, size);
131+
require(isValidSignature(_hash, addr, signature), "ModuleAuthDynamic#_signatureValidation: INVALID_SIGNATURE");
132+
133+
// Acumulate total weight of the signature
134+
totalWeight += addrWeight;
135+
} else {
136+
revert("ModuleAuthDynamic#_signatureValidation INVALID_FLAG");
137+
}
138+
139+
// Defensive check: compare extracted address with target address
140+
if (IMMUTABLE_SIGNER_CONTRACT != address(0) && addr == IMMUTABLE_SIGNER_CONTRACT) {
141+
immutableSignerContractFound = true;
142+
}
143+
144+
// Write weight and address to image
145+
imageHash = keccak256(abi.encode(imageHash, addrWeight, addr));
146+
}
147+
148+
// Check if this is the first transaction (nonce == 0) and immutable signer contract is one of the signers
149+
uint256 currentNonce = uint256(ModuleStorage.readBytes32Map(NonceKey.NONCE_KEY, bytes32(uint256(0))));
150+
if (currentNonce == 0 && immutableSignerContractFound) {
151+
return (true, true, imageHash);
152+
}
153+
154+
(bool verified, bool needsUpdate) = _isValidImage(imageHash);
155+
return ((totalWeight >= threshold && verified), needsUpdate, imageHash);
20156
}
21157

22158
/**

0 commit comments

Comments
 (0)