Skip to content

Commit 2eb132b

Browse files
committed
Finish PoC, let's see if it compiles
1 parent 06a5bb7 commit 2eb132b

File tree

2 files changed

+208
-44
lines changed

2 files changed

+208
-44
lines changed

packages/core/solidity/src/erc7579.ts

Lines changed: 204 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Contract } from './contract';
33
import { printContract } from './print';
44
import { defaults as commonDefaults, withCommonDefaults, type CommonOptions } from './common-options';
55
import { defineFunctions } from './utils/define-functions';
6+
import { requireAccessControl, setAccessControl } from './set-access-control';
67

78
export 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-
7868
function 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

197185
function 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

235369
const 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
};

packages/ui/src/solidity/ERC7579Controls.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
</label>
5050
<label class:checked={opts.validator?.signature} class="subcontrol">
5151
<input
52-
type="checkbox"
52+
type="radio"
5353
checked={opts.validator?.signature}
5454
on:change={e => {
5555
if (e.currentTarget?.checked) {
@@ -64,7 +64,7 @@
6464
</label>
6565
<label class:checked={opts.validator?.multisig} class="subcontrol">
6666
<input
67-
type="checkbox"
67+
type="radio"
6868
checked={opts.validator?.multisig}
6969
on:change={e => {
7070
if (e.currentTarget?.checked) {
@@ -85,6 +85,7 @@
8585
on:change={e => {
8686
if (e.currentTarget?.checked) {
8787
opts.validator ??= {};
88+
opts.validator.signature = false;
8889
opts.validator.multisig ??= {
8990
weighted: true,
9091
};
@@ -103,6 +104,7 @@
103104
on:change={e => {
104105
if (e.currentTarget?.checked) {
105106
opts.validator ??= {};
107+
opts.validator.signature = false;
106108
opts.validator.multisig ??= {
107109
confirmation: true,
108110
};

0 commit comments

Comments
 (0)