@@ -3,6 +3,7 @@ import type { Contract } from './contract';
33import { printContract } from './print' ;
44import { defaults as commonDefaults , withCommonDefaults , type CommonOptions } from './common-options' ;
55import { defineFunctions } from './utils/define-functions' ;
6+ import { requireAccessControl , setAccessControl } from './set-access-control' ;
67
78export const defaults : Required < ERC7579Options > = {
89 ...commonDefaults ,
@@ -56,73 +57,60 @@ export function buildERC7579(opts: ERC7579Options): Contract {
5657
5758 const c = new ContractBuilder ( allOpts . name ) ;
5859
59- // Base parent
60- c . addOverride (
61- {
62- name : 'IERC7579Module' ,
63- } ,
64- functions . isModuleType ,
65- ) ;
66-
67- overrideIsModuleType ( c , allOpts ) ;
6860 addParents ( c , allOpts ) ;
61+ overrideIsModuleType ( c , allOpts ) ;
6962 overrideValidation ( c , allOpts ) ;
70- // addAccess(c, allOpts); TODO
71- // addOnInstall(c, allOpts); TODO
63+ addInstallFns ( c , allOpts ) ;
7264
7365 return c ;
7466}
7567
76- type IsModuleTypeImplementation = 'ERC7579Executor' | 'ERC7579Validator' | 'IERC7579Hook' | 'Fallback' ;
77-
7868function overrideIsModuleType ( c : ContractBuilder , opts : ERC7579Options ) : void {
79- const implementedIn : IsModuleTypeImplementation [ ] = [ 'ERC7579Executor' , 'ERC7579Validator' ] as const ;
80- const types : IsModuleTypeImplementation [ ] = [ ] ;
8169 const fn = functions . isModuleType ;
8270
8371 if ( opts . executor ) {
84- types . push ( 'ERC7579Executor' ) ;
8572 c . addOverride ( { name : 'ERC7579Executor' } , fn ) ;
8673 }
8774
8875 if ( opts . validator ) {
89- types . push ( 'ERC7579Validator' ) ;
9076 c . addOverride ( { name : 'ERC7579Validator' } , fn ) ;
9177 }
9278
9379 if ( opts . hook ) {
94- types . push ( 'IERC7579Hook' ) ;
9580 c . addOverride ( { name : 'IERC7579Hook' } , fn ) ;
9681 }
9782
9883 if ( opts . fallback ) {
99- types . push ( 'Fallback' ) ;
84+ c . addOverride ( { name : 'IERC7579Module' } , fn ) ;
10085 }
10186
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 ) ) ;
10492
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' ;
10897 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 {
113101 const body : string [ ] = [ ] ;
114102 for ( const type of implementedOverrides ) {
115103 body . push ( `bool is${ type } = ${ type } .isModuleType(${ fn . args [ 0 ] ! . name } )` ) ;
116104 }
117105 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' ;
119107 c . addImportOnly ( {
120108 name : importedType ,
121109 path : '@openzeppelin/contracts/interfaces/draft-IERC7579.sol' ,
122110 } ) ;
123111 body . push ( `bool is${ type } = ${ fn . args [ 0 ] ! . name } == ${ importedType } ;` ) ;
124112 }
125- body . push ( `return ${ types . map ( type => `is${ type } ` ) . join ( ' || ' ) } ;` ) ;
113+ body . push ( `return ${ allOverrides . map ( type => `is${ type } ` ) . join ( ' || ' ) } ;` ) ;
126114 c . setFunctionBody ( body , fn ) ;
127115 }
128116}
@@ -195,10 +183,15 @@ function addParents(c: ContractBuilder, opts: ERC7579Options): void {
195183}
196184
197185function overrideValidation ( c : ContractBuilder , opts : ERC7579Options ) : void {
186+ if ( opts . access ) setAccessControl ( c , opts . access ) ;
198187 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.
200189 const fn = delayed ? functions . _validateSchedule : functions . _validateExecution ;
201190 c . addOverride ( c , fn ) ;
191+ c . setFunctionComments (
192+ [ '/// @dev Data is encoded as `[uint16(executionCalldatalLength), executionCalldata, signature]`' ] ,
193+ fn ,
194+ ) ;
202195 if ( opts . validator ) {
203196 c . addParent (
204197 {
@@ -208,18 +201,46 @@ function overrideValidation(c: ContractBuilder, opts: ERC7579Options): void {
208201 [ opts . name , '1' ] ,
209202 ) ;
210203 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)"` ,
222205 ) ;
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' ) ;
223244 } else {
224245 c . setFunctionBody (
225246 [
@@ -230,6 +251,119 @@ function overrideValidation(c: ContractBuilder, opts: ERC7579Options): void {
230251 ) ;
231252 }
232253 }
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+ }
233367}
234368
235369const functions = {
@@ -255,11 +389,39 @@ const functions = {
255389 { name : 'data' , type : 'bytes calldata' } ,
256390 ] ,
257391 } ,
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+ } ,
258412 isModuleType : {
259413 kind : 'public' as const ,
260414 mutability : 'pure' ,
261415 args : [ { name : 'moduleTypeId' , type : 'uint256' } ] ,
262416 returns : [ 'bool' ] ,
263417 } ,
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+ } ,
264426 } ) ,
265427} ;
0 commit comments