@@ -3,6 +3,7 @@ import type { Contract } from './contract';
3
3
import { printContract } from './print' ;
4
4
import { defaults as commonDefaults , withCommonDefaults , type CommonOptions } from './common-options' ;
5
5
import { defineFunctions } from './utils/define-functions' ;
6
+ import { requireAccessControl , setAccessControl } from './set-access-control' ;
6
7
7
8
export const defaults : Required < ERC7579Options > = {
8
9
...commonDefaults ,
@@ -56,73 +57,60 @@ export function buildERC7579(opts: ERC7579Options): Contract {
56
57
57
58
const c = new ContractBuilder ( allOpts . name ) ;
58
59
59
- // Base parent
60
- c . addOverride (
61
- {
62
- name : 'IERC7579Module' ,
63
- } ,
64
- functions . isModuleType ,
65
- ) ;
66
-
67
- overrideIsModuleType ( c , allOpts ) ;
68
60
addParents ( c , allOpts ) ;
61
+ overrideIsModuleType ( c , allOpts ) ;
69
62
overrideValidation ( c , allOpts ) ;
70
- // addAccess(c, allOpts); TODO
71
- // addOnInstall(c, allOpts); TODO
63
+ addInstallFns ( c , allOpts ) ;
72
64
73
65
return c ;
74
66
}
75
67
76
- type IsModuleTypeImplementation = 'ERC7579Executor' | 'ERC7579Validator' | 'IERC7579Hook' | 'Fallback' ;
77
-
78
68
function overrideIsModuleType ( c : ContractBuilder , opts : ERC7579Options ) : void {
79
- const implementedIn : IsModuleTypeImplementation [ ] = [ 'ERC7579Executor' , 'ERC7579Validator' ] as const ;
80
- const types : IsModuleTypeImplementation [ ] = [ ] ;
81
69
const fn = functions . isModuleType ;
82
70
83
71
if ( opts . executor ) {
84
- types . push ( 'ERC7579Executor' ) ;
85
72
c . addOverride ( { name : 'ERC7579Executor' } , fn ) ;
86
73
}
87
74
88
75
if ( opts . validator ) {
89
- types . push ( 'ERC7579Validator' ) ;
90
76
c . addOverride ( { name : 'ERC7579Validator' } , fn ) ;
91
77
}
92
78
93
79
if ( opts . hook ) {
94
- types . push ( 'IERC7579Hook' ) ;
95
80
c . addOverride ( { name : 'IERC7579Hook' } , fn ) ;
96
81
}
97
82
98
83
if ( opts . fallback ) {
99
- types . push ( 'Fallback' ) ;
84
+ c . addOverride ( { name : 'IERC7579Module' } , fn ) ;
100
85
}
101
86
102
- const implementedOverrides = types . filter ( type => implementedIn . includes ( type ) ) ;
103
- const unimplementedOverrides = types . filter ( type => ! implementedIn . includes ( type ) ) ;
87
+ const implementedIn = [ 'ERC7579Executor' , 'ERC7579Validator' ] ;
88
+ const contractFn = c . functions . find ( f => f . name === 'isModuleType' ) ! ;
89
+ const allOverrides = Array . from ( contractFn ?. override . values ( ) ?? [ ] ) . map ( v => v . name ) ;
90
+ const implementedOverrides = allOverrides . filter ( type => implementedIn . includes ( type ) ) ;
91
+ const unimplementedOverrides = allOverrides . filter ( type => ! implementedIn . includes ( type ) ) ;
104
92
105
- if ( implementedOverrides . length === 0 && unimplementedOverrides . length === 1 ) {
106
- const importedType =
107
- unimplementedOverrides [ 0 ] ! === 'IERC7579Hook' ? 'MODULE_TYPE_VALIDATOR' : 'MODULE_TYPE_FALLBACK' ;
93
+ if ( ! implementedOverrides . length && ! unimplementedOverrides . length ) {
94
+ c . setFunctionBody ( [ 'return false;' ] , fn ) ;
95
+ } else if ( ! implementedOverrides . length && unimplementedOverrides . length === 1 ) {
96
+ const importedType = unimplementedOverrides [ 0 ] ! === 'IERC7579Hook' ? 'MODULE_TYPE_HOOK' : 'MODULE_TYPE_FALLBACK' ;
108
97
c . setFunctionBody ( [ `return ${ fn . args [ 0 ] ! . name } == ${ importedType } ;` ] , fn ) ;
109
- } else if (
110
- implementedOverrides . length >= 2 || // 1 = n/a, 2 = defaults to super
111
- unimplementedOverrides . length > 0 // Require manual comparison
112
- ) {
98
+ } else if ( implementedOverrides . length == 1 && ! unimplementedOverrides . length ) {
99
+ c . setFunctionBody ( [ `return ${ implementedOverrides [ 0 ] ! } .isModuleType(${ fn . args [ 0 ] ! . name } )` ] , fn ) ;
100
+ } else {
113
101
const body : string [ ] = [ ] ;
114
102
for ( const type of implementedOverrides ) {
115
103
body . push ( `bool is${ type } = ${ type } .isModuleType(${ fn . args [ 0 ] ! . name } )` ) ;
116
104
}
117
105
for ( const type of unimplementedOverrides ) {
118
- const importedType = type === 'IERC7579Hook' ? 'MODULE_TYPE_VALIDATOR ' : 'MODULE_TYPE_FALLBACK' ;
106
+ const importedType = type === 'IERC7579Hook' ? 'MODULE_TYPE_HOOK ' : 'MODULE_TYPE_FALLBACK' ;
119
107
c . addImportOnly ( {
120
108
name : importedType ,
121
109
path : '@openzeppelin/contracts/interfaces/draft-IERC7579.sol' ,
122
110
} ) ;
123
111
body . push ( `bool is${ type } = ${ fn . args [ 0 ] ! . name } == ${ importedType } ;` ) ;
124
112
}
125
- body . push ( `return ${ types . map ( type => `is${ type } ` ) . join ( ' || ' ) } ;` ) ;
113
+ body . push ( `return ${ allOverrides . map ( type => `is${ type } ` ) . join ( ' || ' ) } ;` ) ;
126
114
c . setFunctionBody ( body , fn ) ;
127
115
}
128
116
}
@@ -195,10 +183,15 @@ function addParents(c: ContractBuilder, opts: ERC7579Options): void {
195
183
}
196
184
197
185
function overrideValidation ( c : ContractBuilder , opts : ERC7579Options ) : void {
186
+ if ( opts . access ) setAccessControl ( c , opts . access ) ;
198
187
if ( opts . executor ) {
199
- const delayed = ! opts . executor . delayed ; // Delayed ensures single execution per operation.
188
+ const delayed = opts . executor . delayed ; // Delayed ensures single execution per operation.
200
189
const fn = delayed ? functions . _validateSchedule : functions . _validateExecution ;
201
190
c . addOverride ( c , fn ) ;
191
+ c . setFunctionComments (
192
+ [ '/// @dev Data is encoded as `[uint16(executionCalldatalLength), executionCalldata, signature]`' ] ,
193
+ fn ,
194
+ ) ;
202
195
if ( opts . validator ) {
203
196
c . addParent (
204
197
{
@@ -208,18 +201,46 @@ function overrideValidation(c: ContractBuilder, opts: ERC7579Options): void {
208
201
[ opts . name , '1' ] ,
209
202
) ;
210
203
c . addVariable (
211
- `bytes32 public constant EXECUTION_TYPEHASH = "Execute(address account,bytes32 salt,${ delayed ? 'uint256 nonce,' : '' } bytes32 mode,bytes executionCalldata)"` ,
212
- ) ;
213
- c . setFunctionBody (
214
- [
215
- `uint16 executionCalldataLength = uint16(uint256(bytes32(${ fn . args [ 3 ] ! . name } [0:2]))); // First 2 bytes are the length` ,
216
- `bytes calldata executionCalldata = ${ fn . args [ 3 ] ! . name } [2:2 + executionCalldataLength]; // Next bytes are the calldata` ,
217
- `bytes32 typeHash = _hashTypedDataV4(keccak256(abi.encode(EXECUTION_TYPEHASH, ${ fn . args [ 0 ] ! . name } , ${ fn . args [ 1 ] ! . name } ,${ delayed ? ` _useNonce(${ fn . args [ 0 ] ! . name } ),` : '' } ${ fn . args [ 2 ] ! . name } , executionCalldata)));` ,
218
- `require(_rawERC7579Validation(${ fn . args [ 0 ] ! . name } , typeHash, ${ fn . args [ 3 ] ! . name } [2 + executionCalldataLength:])); // Remaining bytes are the signature` ,
219
- `return executionCalldata;` ,
220
- ] ,
221
- fn ,
204
+ `bytes32 public constant EXECUTION_TYPEHASH = "Execute(address account,bytes32 salt,${ ! delayed ? 'uint256 nonce,' : '' } bytes32 mode,bytes executionCalldata)"` ,
222
205
) ;
206
+ const body = [
207
+ `uint16 executionCalldataLength = uint16(uint256(bytes32(${ fn . args [ 3 ] ! . name } [0:2]))); // First 2 bytes are the length` ,
208
+ `bytes calldata executionCalldata = ${ fn . args [ 3 ] ! . name } [2:2 + executionCalldataLength]; // Next bytes are the calldata` ,
209
+ `bytes32 typeHash = _hashTypedDataV4(keccak256(abi.encode(EXECUTION_TYPEHASH, ${ fn . args [ 0 ] ! . name } , ${ fn . args [ 1 ] ! . name } ,${ ! delayed ? ` _useNonce(${ fn . args [ 0 ] ! . name } ),` : '' } ${ fn . args [ 2 ] ! . name } , executionCalldata)));` ,
210
+ ] ;
211
+ const conditions = [
212
+ `_rawERC7579Validation(${ fn . args [ 0 ] ! . name } , typeHash, ${ fn . args [ 3 ] ! . name } [2 + executionCalldataLength:])` ,
213
+ ] ;
214
+ switch ( opts . access ) {
215
+ case 'ownable' :
216
+ conditions . unshift ( 'msg.sender == owner()' ) ;
217
+ break ;
218
+ case 'roles' : {
219
+ const roleOwner = 'executor' ;
220
+ const roleId = 'EXECUTOR_ROLE' ;
221
+ c . addVariable ( `bytes32 public constant ${ roleId } = keccak256("${ roleId } ");` ) ;
222
+ c . addConstructorArgument ( { type : 'address' , name : roleOwner } ) ;
223
+ c . addConstructorCode ( `_grantRole(${ roleId } , ${ roleOwner } );` ) ;
224
+ conditions . unshift ( `hasRole(${ roleId } , msg.sender)` ) ;
225
+ break ;
226
+ }
227
+ case 'managed' :
228
+ c . addImportOnly ( {
229
+ name : 'AuthorityUtils' ,
230
+ path : `@openzeppelin/contracts/access/manager/AuthorityUtils.sol` ,
231
+ } ) ;
232
+ body . push (
233
+ `(bool immediate, ) = AuthorityUtils.canCallWithDelay(authority(), msg.sender, address(this), bytes4(msg.data[0:4]));` ,
234
+ ) ;
235
+ conditions . unshift ( 'immediate' ) ;
236
+ break ;
237
+ default :
238
+ }
239
+ body . push ( `require(${ conditions . join ( ' || ' ) } );` ) ;
240
+ if ( ! delayed ) body . push ( `return executionCalldata;` ) ;
241
+ c . setFunctionBody ( body , fn ) ;
242
+ } else if ( opts . access ) {
243
+ requireAccessControl ( c , fn , opts . access , 'EXECUTOR' , 'executor' ) ;
223
244
} else {
224
245
c . setFunctionBody (
225
246
[
@@ -230,6 +251,119 @@ function overrideValidation(c: ContractBuilder, opts: ERC7579Options): void {
230
251
) ;
231
252
}
232
253
}
254
+ if ( opts . validator ) {
255
+ const isValidFn = functions . isValidSignatureWithSender ;
256
+ const fnSuper = `super.${ isValidFn . name } (${ isValidFn . args . map ( a => a . name ) . join ( ', ' ) } )` ;
257
+ c . addOverride ( c , isValidFn ) ;
258
+
259
+ if ( ! opts . validator . multisig && opts . validator . signature ) {
260
+ c . setFunctionBody ( [ 'return false;' ] , functions . _rawERC7579Validation ) ;
261
+ }
262
+
263
+ switch ( opts . access ) {
264
+ case 'ownable' :
265
+ c . setFunctionBody ( [ `return owner() == ${ isValidFn . args [ 0 ] ! . name } || ${ fnSuper } ;` ] , isValidFn ) ;
266
+ break ;
267
+ case 'roles' : {
268
+ const roleOwner = 'erc1271ValidSender' ;
269
+ const roleId = 'ERC1271_VALID_SENDER_ROLE' ;
270
+ c . addVariable ( `bytes32 public constant ${ roleId } = keccak256("${ roleId } ");` ) ;
271
+ c . addConstructorArgument ( { type : 'address' , name : roleOwner } ) ;
272
+ c . addConstructorCode ( `_grantRole(${ roleId } , ${ roleOwner } );` ) ;
273
+ c . setFunctionBody ( [ `return hasRole(${ roleId } , ${ isValidFn . args [ 0 ] ! . name } ) || ${ fnSuper } ;` ] , isValidFn ) ;
274
+ break ;
275
+ }
276
+ case 'managed' :
277
+ c . addImportOnly ( {
278
+ name : 'AuthorityUtils' ,
279
+ path : `@openzeppelin/contracts/access/manager/AuthorityUtils.sol` ,
280
+ } ) ;
281
+ c . setFunctionBody (
282
+ [
283
+ `(bool immediate, ) = AuthorityUtils.canCallWithDelay(authority(), ${ isValidFn . args [ 0 ] ! . name } , address(this), bytes4(msg.data[0:4]));` ,
284
+ `return immediate || ${ fnSuper } ;` ,
285
+ ] ,
286
+ isValidFn ,
287
+ ) ;
288
+ break ;
289
+ default :
290
+ }
291
+ }
292
+ }
293
+
294
+ function addInstallFns ( c : ContractBuilder , opts : ERC7579Options ) : void {
295
+ if ( opts . validator ?. signature ) {
296
+ c . addOverride ( { name : 'ERC7579Signature' } , functions . onInstall ) ;
297
+ c . addOverride ( { name : 'ERC7579Signature' } , functions . onUninstall ) ;
298
+ }
299
+
300
+ if ( opts . validator ?. multisig ) {
301
+ const name = opts . validator . multisig . weighted ? 'ERC7579MultisigWeighted' : 'ERC7579Multisig' ;
302
+ c . addOverride ( { name } , functions . onInstall ) ;
303
+ c . addOverride ( { name } , functions . onUninstall ) ;
304
+ }
305
+
306
+ if ( opts . executor ?. delayed ) {
307
+ c . addOverride ( { name : 'ERC7579DelayedExecutor' } , functions . onInstall ) ;
308
+ c . addOverride ( { name : 'ERC7579DelayedExecutor' } , functions . onUninstall ) ;
309
+ }
310
+
311
+ const onInstallFn = c . functions . find ( f => f . name === 'onInstall' ) ;
312
+ const allOnInstallOverrides = Array . from ( onInstallFn ?. override . values ( ) ?? [ ] ) . map ( c => c . name ) ;
313
+ buildOnInstallFn ( c , allOnInstallOverrides ) ;
314
+
315
+ const onUninstallFn = c . functions . find ( f => f . name === 'onUninstall' ) ;
316
+ const allOnUninstallOverrides = Array . from ( onUninstallFn ?. override . values ( ) ?? [ ] ) . map ( c => c . name ) ;
317
+ buildOnUninstallFn ( c , allOnUninstallOverrides ) ;
318
+ }
319
+
320
+ function buildOnInstallFn ( c : ContractBuilder , overrides : string [ ] ) {
321
+ const fn = functions . onInstall ;
322
+ if ( ! overrides . length ) {
323
+ c . setFunctionBody ( [ '// Use `data` to initialize' ] , fn ) ;
324
+ }
325
+ // overrides.length == 1 will use super by default
326
+ else if ( overrides . length >= 2 ) {
327
+ const body : string [ ] = [ ] ;
328
+ let lengthOffset = '0' ;
329
+ let comment = '/// @dev Data is encoded as `[' ;
330
+
331
+ for ( const [ i , name ] of overrides . entries ( ) ) {
332
+ const argsName = `args${ name } ` ;
333
+ const lengthName = `${ argsName } Length` ;
334
+ const argsOffset = ! i ? '2' : `${ lengthOffset } + 2` ;
335
+ const restOffset = `${ argsOffset } + ${ lengthName } ` ;
336
+ comment += `uint16(${ lengthName } ), ${ argsName } ` ;
337
+ body . push (
338
+ `uint16 ${ lengthName } = uint16(uint256(bytes32(${ fn . args [ 0 ] ! . name } [${ lengthOffset } :${ argsOffset } ]))); // First 2 bytes are the length` ,
339
+ `bytes calldata ${ argsName } = ${ fn . args [ 0 ] ! . name } [${ argsOffset } :${ restOffset } ]; // Next bytes are the args` ,
340
+ `${ name } .onInstall(${ argsName } );` ,
341
+ ) ;
342
+ if ( i != overrides . length - 1 ) {
343
+ body . push ( '' ) ;
344
+ comment += ', ' ;
345
+ }
346
+ lengthOffset = restOffset ;
347
+ }
348
+ c . setFunctionComments ( [ `${ comment } ]` ] , fn ) ;
349
+ c . setFunctionBody ( body , fn ) ;
350
+ }
351
+ }
352
+
353
+ function buildOnUninstallFn ( c : ContractBuilder , overrides : string [ ] ) {
354
+ const fn = functions . onUninstall ;
355
+ if ( ! overrides . length ) {
356
+ c . setFunctionBody ( [ '// Use `data` to deinitialize' ] , fn ) ;
357
+ }
358
+ // overrides.length == 1 will use super by default
359
+ else if ( overrides . length >= 2 ) {
360
+ c . addImportOnly ( { name : 'Calldata' , path : '@openzeppelin/contracts/utils/Calldata.sol' } ) ;
361
+ const body : string [ ] = [ ] ;
362
+ for ( const name of overrides ) {
363
+ body . push ( `${ name } .onUninstall(Calldata.emptyBytes());` ) ;
364
+ }
365
+ c . setFunctionBody ( body , fn ) ;
366
+ }
233
367
}
234
368
235
369
const functions = {
@@ -255,11 +389,39 @@ const functions = {
255
389
{ name : 'data' , type : 'bytes calldata' } ,
256
390
] ,
257
391
} ,
392
+ isValidSignatureWithSender : {
393
+ kind : 'public' as const ,
394
+ mutability : 'view' ,
395
+ args : [
396
+ { name : 'sender' , type : 'address' } ,
397
+ { name : 'hash' , type : 'bytes32' } ,
398
+ { name : 'signature' , type : 'bytes calldata' } ,
399
+ ] ,
400
+ returns : [ 'bytes4' ] ,
401
+ } ,
402
+ _rawERC7579Validation : {
403
+ kind : 'internal' as const ,
404
+ mutability : 'view' ,
405
+ args : [
406
+ { name : 'account' , type : 'address' } ,
407
+ { name : 'hash' , type : 'bytes32' } ,
408
+ { name : 'signature' , type : 'bytes calldata' } ,
409
+ ] ,
410
+ returns : [ 'bool' ] ,
411
+ } ,
258
412
isModuleType : {
259
413
kind : 'public' as const ,
260
414
mutability : 'pure' ,
261
415
args : [ { name : 'moduleTypeId' , type : 'uint256' } ] ,
262
416
returns : [ 'bool' ] ,
263
417
} ,
418
+ onInstall : {
419
+ kind : 'public' as const ,
420
+ args : [ { name : 'data' , type : 'bytes calldata' } ] ,
421
+ } ,
422
+ onUninstall : {
423
+ kind : 'public' as const ,
424
+ args : [ { name : 'data' , type : 'bytes calldata' } ] ,
425
+ } ,
264
426
} ) ,
265
427
} ;
0 commit comments