Skip to content

Commit f44e96d

Browse files
committed
Add ERC7579 modules
1 parent 3a20092 commit f44e96d

File tree

15 files changed

+399
-6
lines changed

15 files changed

+399
-6
lines changed

packages/core/solidity/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Add `erc7579` contract types for ERC-7579 modules.
36

47
## 0.5.5 (2025-05-13)
58

packages/core/solidity/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The following contract types are supported:
2222
- `governor`
2323
- `custom`
2424

25-
Note that `stablecoin`, `realWorldAsset`, and `account` are experimental and may be subject to change.
25+
Note that `stablecoin`, `realWorldAsset`, `account`, and `erc7579` are experimental and may be subject to change.
2626

2727
Each contract type has functions/constants as defined below.
2828

@@ -45,6 +45,9 @@ function print(opts?: StablecoinOptions): string
4545
function print(opts?: AccountOptions): string
4646
```
4747
```js
48+
function print(opts?: ERC7579Options): string
49+
```
50+
```js
4851
function print(opts?: GovernorOptions): string
4952
```
5053
```js
@@ -69,6 +72,9 @@ const defaults: Required<StablecoinOptions>
6972
const defaults: Required<AccountOptions>
7073
```
7174
```js
75+
const defaults: Required<ERC7579Options>
76+
```
77+
```js
7278
const defaults: Required<GovernorOptions>
7379
```
7480
```js

packages/core/solidity/src/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
} from './stablecoin';
2626
import type { AccountOptions } from './account';
2727
import { printAccount, defaults as accountDefaults } from './account';
28+
import type { ERC7579Options } from './erc7579';
29+
import { printERC7579, defaults as erc7579Defaults } from './erc7579';
2830
import type { GovernorOptions } from './governor';
2931
import {
3032
printGovernor,
@@ -64,6 +66,7 @@ export type ERC1155 = WizardContractAPI<ERC1155Options> & AccessControlAPI<ERC11
6466
export type Stablecoin = WizardContractAPI<StablecoinOptions> & AccessControlAPI<StablecoinOptions>;
6567
export type RealWorldAsset = WizardContractAPI<StablecoinOptions> & AccessControlAPI<StablecoinOptions>;
6668
export type Account = WizardContractAPI<AccountOptions>;
69+
export type ERC7579 = WizardContractAPI<ERC7579Options>;
6770
export type Governor = WizardContractAPI<GovernorOptions> & AccessControlAPI<GovernorOptions>;
6871
export type Custom = WizardContractAPI<CustomOptions> & AccessControlAPI<CustomOptions>;
6972

@@ -91,6 +94,10 @@ export const account: Account = {
9194
print: printAccount,
9295
defaults: accountDefaults,
9396
};
97+
export const erc7579: ERC7579 = {
98+
print: printERC7579,
99+
defaults: erc7579Defaults,
100+
};
94101
export const realWorldAsset: RealWorldAsset = {
95102
print: printStablecoin,
96103
defaults: stablecoinDefaults,

packages/core/solidity/src/build-generic.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { buildGovernor } from './governor';
1313
import type { Contract } from './contract';
1414
import { buildAccount } from './account';
1515
import type { AccountOptions } from './account';
16+
import { buildERC7579 } from './erc7579';
17+
import type { ERC7579Options } from './erc7579';
1618

1719
export interface KindedOptions {
1820
ERC20: { kind: 'ERC20' } & ERC20Options;
@@ -21,6 +23,7 @@ export interface KindedOptions {
2123
Stablecoin: { kind: 'Stablecoin' } & StablecoinOptions;
2224
RealWorldAsset: { kind: 'RealWorldAsset' } & StablecoinOptions;
2325
Account: { kind: 'Account' } & AccountOptions;
26+
ERC7579: { kind: 'ERC7579' } & ERC7579Options;
2427
Governor: { kind: 'Governor' } & GovernorOptions;
2528
Custom: { kind: 'Custom' } & CustomOptions;
2629
}
@@ -47,6 +50,9 @@ export function buildGeneric(opts: GenericOptions): Contract {
4750
case 'Account':
4851
return buildAccount(opts);
4952

53+
case 'ERC7579':
54+
return buildERC7579(opts);
55+
5056
case 'Governor':
5157
return buildGovernor(opts);
5258

packages/core/solidity/src/erc7579.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { ContractBuilder } from './contract';
2+
import type { Contract } from './contract';
3+
import { printContract } from './print';
4+
import { defaults as commonDefaults, withCommonDefaults, type CommonOptions } from './common-options';
5+
import { defineFunctions } from './utils/define-functions';
6+
7+
export const defaults: Required<ERC7579Options> = {
8+
...commonDefaults,
9+
name: 'MyERC7579Module',
10+
validator: undefined,
11+
executor: undefined,
12+
hook: false,
13+
fallback: false,
14+
access: commonDefaults.access,
15+
} as const;
16+
17+
export type ERC7579MultisigType = {
18+
weighted: boolean;
19+
confirmation: boolean;
20+
};
21+
22+
export type ERC7579ValidatorType = {
23+
signature: boolean;
24+
multisig: ERC7579MultisigType;
25+
};
26+
27+
export type ERC7579ExecutorType = {
28+
delayed: boolean;
29+
};
30+
31+
export interface ERC7579Options extends CommonOptions {
32+
name: string;
33+
validator: ERC7579ValidatorType | undefined;
34+
executor: ERC7579ExecutorType | undefined;
35+
hook: boolean;
36+
fallback: boolean;
37+
}
38+
39+
function withDefaults(opts: ERC7579Options): Required<ERC7579Options> {
40+
return {
41+
...withCommonDefaults(opts),
42+
name: opts.name ?? defaults.name,
43+
validator: opts.validator ?? defaults.validator,
44+
executor: opts.executor ?? defaults.executor,
45+
hook: opts.hook ?? defaults.hook,
46+
fallback: opts.fallback ?? defaults.fallback,
47+
};
48+
}
49+
50+
export function printERC7579(opts: ERC7579Options = defaults): string {
51+
return printContract(buildERC7579(opts));
52+
}
53+
54+
export function buildERC7579(opts: ERC7579Options): Contract {
55+
const allOpts = withDefaults(opts);
56+
57+
const c = new ContractBuilder(allOpts.name);
58+
59+
addParents(c, allOpts);
60+
addMultisig(c, allOpts);
61+
62+
return c;
63+
}
64+
65+
function addParents(c: ContractBuilder, opts: ERC7579Options): void {
66+
c.addParent({
67+
name: 'IERC7579Module',
68+
path: '@openzeppelin/contracts/interfaces/draft-IERC7579.sol',
69+
});
70+
71+
if (opts.executor) {
72+
c.addParent({
73+
name: 'ERC7579Executor',
74+
path: '@openzeppelin/community-contracts/account/modules/ERC7579Executor.sol',
75+
});
76+
77+
if (opts.executor.delayed) {
78+
c.addParent({
79+
name: 'ERC7579DelayedExecutor',
80+
path: '@openzeppelin/community-contracts/account/modules/ERC7579DelayedExecutor.sol',
81+
});
82+
}
83+
}
84+
85+
if (opts.validator) {
86+
c.addParent({
87+
name: 'ERC7579Validator',
88+
path: '@openzeppelin/community-contracts/account/modules/ERC7579Validator.sol',
89+
});
90+
91+
if (opts.validator.multisig) {
92+
c.addParent({
93+
name: 'ERC7579Multisig',
94+
path: '@openzeppelin/community-contracts/account/modules/ERC7579Multisig.sol',
95+
});
96+
97+
if (opts.validator.multisig.weighted) {
98+
c.addParent({
99+
name: 'ERC7579MultisigWeighted',
100+
path: '@openzeppelin/community-contracts/account/modules/ERC7579MultisigWeighted.sol',
101+
});
102+
}
103+
104+
if (opts.validator.multisig.confirmation) {
105+
c.addParent({
106+
name: 'ERC7579MultisigConfirmation',
107+
path: '@openzeppelin/community-contracts/account/modules/ERC7579MultisigConfirmation.sol',
108+
});
109+
}
110+
}
111+
}
112+
113+
if (opts.hook) {
114+
c.addParent({
115+
name: 'IERC7579Hook',
116+
path: '@openzeppelin/contracts/interfaces/draft-IERC7579.sol',
117+
});
118+
}
119+
120+
if (opts.fallback) {
121+
// NO OP
122+
}
123+
}
124+
125+
function addMultisig(c: ContractBuilder, opts: ERC7579Options): void {
126+
if (opts.executor) {
127+
const fn = functions._validateExecution;
128+
c.addOverride(c, fn);
129+
if (opts.validator) {
130+
// _rawERC7579Validation available
131+
c.setFunctionBody(
132+
[
133+
`uint16 executionCalldataLength = uint16(uint256(bytes32(${fn.args[3]!.name}[0:2]))); // First 2 bytes are the length`,
134+
`bytes calldata executionCalldata = ${fn.args[3]!.name}[2:2 + executionCalldataLength]; // Next bytes are the calldata`,
135+
`bytes32 typeHash = _getExecuteTypeHash(${fn.args[0]!.name}, salt, mode, executionCalldata);`,
136+
`require(_rawERC7579Validation(${fn.args[0]!.name}, typeHash, ${fn.args[3]!.name}[2 + executionCalldataLength:])); // Remaining bytes are the signature`,
137+
`return executionCalldata;`,
138+
],
139+
fn,
140+
);
141+
} else {
142+
c.setFunctionBody(
143+
[
144+
`// Slice \`${fn.args[3]!.name}\` to build custom authorization based on calldata`,
145+
`return ${fn.args[3]!.name}; // Use raw ${fn.args[3]!.name} as execution calldata`,
146+
],
147+
fn,
148+
);
149+
}
150+
}
151+
}
152+
153+
const functions = {
154+
...defineFunctions({
155+
_validateExecution: {
156+
kind: 'internal' as const,
157+
args: [
158+
{ name: 'account', type: 'address' },
159+
{ name: 'hash', type: 'bytes32' },
160+
{ name: 'mode', type: 'bytes32' },
161+
{ name: 'data', type: 'bytes calldata' },
162+
],
163+
returns: ['bytes calldata'],
164+
},
165+
}),
166+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { ERC7579Options } from '../erc7579';
2+
import { accessOptions } from '../set-access-control';
3+
import { infoOptions } from '../set-info';
4+
import { generateAlternatives } from './alternatives';
5+
6+
const erc7579 = {
7+
name: ['MyERC7579'],
8+
type: [false, 'Validator', 'Executor', 'DelayedExecutor', 'FallbackHandler', 'Hook'] as const,
9+
multisig: [false, 'ERC7579Multisig', 'ERC7579MultisigConfirmation', 'ERC7579MultisigWeighted'] as const,
10+
access: accessOptions,
11+
upgradeable: [false] as const,
12+
info: infoOptions,
13+
};
14+
15+
export function* generateERC7579Options(): Generator<Required<ERC7579Options>> {
16+
yield* generateAlternatives(erc7579);
17+
}

packages/core/solidity/src/generate/sources.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { generateERC721Options } from './erc721';
77
import { generateERC1155Options } from './erc1155';
88
import { generateStablecoinOptions } from './stablecoin';
99
import { generateAccountOptions } from './account';
10+
import { generateERC7579Options } from './erc7579';
1011
import { generateGovernorOptions } from './governor';
1112
import { generateCustomOptions } from './custom';
1213
import type { GenericOptions, KindedOptions } from '../build-generic';
@@ -57,6 +58,12 @@ export function* generateOptions(kind?: Kind): Generator<GenericOptions> {
5758
}
5859
}
5960

61+
if (!kind || kind === 'ERC7579') {
62+
for (const kindOpts of generateERC7579Options()) {
63+
yield { kind: 'ERC7579', ...kindOpts };
64+
}
65+
}
66+
6067
if (!kind || kind === 'Governor') {
6168
for (const kindOpts of generateGovernorOptions()) {
6269
yield { kind: 'Governor', ...kindOpts };

packages/core/solidity/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ export { OptionsError } from './error';
1919
export type { Kind } from './kind';
2020
export { sanitizeKind } from './kind';
2121

22-
export { erc20, erc721, erc1155, stablecoin, realWorldAsset, account, governor, custom } from './api';
22+
export { erc20, erc721, erc1155, stablecoin, realWorldAsset, account, erc7579, governor, custom } from './api';
2323

2424
export { compatibleContractsSemver } from './utils/version';

packages/core/solidity/src/kind.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function isKind<T>(value: Kind | T): value is Kind {
2020
case 'Stablecoin':
2121
case 'RealWorldAsset':
2222
case 'Account':
23+
case 'ERC7579':
2324
case 'Governor':
2425
case 'Custom':
2526
return true;

packages/ui/api/ai-assistant/types/languages.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ export type StylusCommonContractOptions = Omit<StylusCommonContractOptionsBase,
2020

2121
// Add supported language here
2222
export type LanguagesContractsOptions = {
23-
solidity: Omit<SolidityKindedOptions, 'Stablecoin' | 'RealWorldAsset' | 'Account'> & {
23+
solidity: Omit<SolidityKindedOptions, 'Stablecoin' | 'RealWorldAsset' | 'Account' | 'ERC7579'> & {
2424
Stablecoin: Omit<SolidityKindedOptions['Stablecoin'], 'upgradeable'> & { upgradeable?: false };
2525
RealWorldAsset: Omit<SolidityKindedOptions['RealWorldAsset'], 'upgradeable'> & { upgradeable?: false };
2626
Account: Omit<SolidityKindedOptions['Account'], 'upgradeable' | 'access'> & { upgradeable?: false; access?: false };
27+
ERC7579: Omit<SolidityKindedOptions['ERC7579'], 'upgradeable'> & { upgradeable?: false };
2728
};
2829
cairo: CairoKindedOptions;
2930
cairoAlpha: CairoAlphaKindedOptions;

0 commit comments

Comments
 (0)