Skip to content

Add multisig to Account Wizard #527

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion packages/core/solidity/src/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ test('account API assert defaults', async t => {
t.is(account.print(account.defaults), account.print());
});

for (const signer of [false, 'ERC7702', 'ECDSA', 'P256', 'RSA'] as const) {
for (const signer of [false, 'ERC7702', 'ECDSA', 'P256', 'RSA', 'Multisig', 'MultisigWeighted'] as const) {
let title = 'Account';
if (signer) {
title += ` with Signer${signer}`;
Expand Down
1,279 changes: 1,279 additions & 0 deletions packages/core/solidity/src/account.test.ts.md

Large diffs are not rendered by default.

Binary file modified packages/core/solidity/src/account.test.ts.snap
Binary file not shown.
37 changes: 37 additions & 0 deletions packages/core/solidity/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function addParents(c: ContractBuilder, opts: AccountOptions): void {
addSignatureValidation(c, opts);
addERC7579Modules(c, opts);
addSigner(c, opts.signer ?? false);
addMultisigFunctions(c, opts);
addBatchedExecution(c, opts);
addERC721Holder(c, opts);
addERC1155Holder(c, opts);
Expand Down Expand Up @@ -175,6 +176,23 @@ function addERC7579Modules(c: ContractBuilder, opts: AccountOptions): void {
);
}

function addMultisigFunctions(c: ContractBuilder, opts: AccountOptions): void {
switch (opts.signer) {
case 'MultisigWeighted':
c.addFunctionCode(
`_setSignerWeights(${functions.setSignerWeights.args.map(({ name }) => name).join(', ')});`,
functions.setSignerWeights,
);
// eslint-disable-next-line no-fallthrough
case 'Multisig':
c.addFunctionCode(`_addSigners(${functions.addSigners.args[0]?.name});`, functions.addSigners);
c.addFunctionCode(`_removeSigners(${functions.removeSigners.args[0]?.name});`, functions.removeSigners);
c.addFunctionCode(`_setThreshold(${functions.setThreshold.args[0]?.name});`, functions.setThreshold);
break;
default:
}
}

function addEIP712(c: ContractBuilder, opts: AccountOptions): void {
if (opts.signatureValidation != 'ERC7739') return;
c.addParent(
Expand Down Expand Up @@ -247,5 +265,24 @@ const functions = {
returns: ['bool'],
mutability: 'view' as const,
},
addSigners: {
kind: 'public' as const,
args: [{ name: 'signers', type: 'bytes[] memory' }],
},
removeSigners: {
kind: 'public' as const,
args: [{ name: 'signers', type: 'bytes[] memory' }],
},
setThreshold: {
kind: 'public' as const,
args: [{ name: 'threshold', type: 'uint256' }],
},
setSignerWeights: {
kind: 'public' as const,
args: [
{ name: 'signers', type: 'bytes[] memory' },
{ name: 'weights', type: 'uint256[] memory' },
],
},
}),
};
2 changes: 1 addition & 1 deletion packages/core/solidity/src/generate/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const account = {
signatureValidation: [false, 'ERC1271', 'ERC7739'] as const,
ERC721Holder: [false, true] as const,
ERC1155Holder: [false, true] as const,
signer: ['ERC7702', 'ECDSA', 'P256', 'RSA'] as const,
signer: ['ERC7702', 'ECDSA', 'P256', 'RSA', 'Multisig', 'MultisigWeighted'] as const,
batchedExecution: [false, true] as const,
ERC7579Modules: [false, 'AccountERC7579', 'AccountERC7579Hooked'] as const,
access: [false] as const,
Expand Down
54 changes: 46 additions & 8 deletions packages/core/solidity/src/signer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ContractBuilder } from './contract';
import { defineFunctions } from './utils/define-functions';

export const SignerOptions = [false, 'ERC7702', 'ECDSA', 'P256', 'RSA'] as const;
export const SignerOptions = [false, 'ERC7702', 'ECDSA', 'P256', 'RSA', 'Multisig', 'MultisigWeighted'] as const;
export type SignerOptions = (typeof SignerOptions)[number];

export function addSigner(c: ContractBuilder, signer: SignerOptions): void {
Expand All @@ -19,13 +19,28 @@ export function addSigner(c: ContractBuilder, signer: SignerOptions): void {
});
const fn = signerFunctions[`initialize${signer}`];
c.addModifier('initializer', fn);
c.addFunctionCode(
`_setSigner(${fn.args
.map(({ name }) => name)
.join(', ')
.trimEnd()});`,
fn,
);

switch (signer) {
case 'Multisig':
c.addFunctionCode(`_addSigners(${fn.args?.[0]?.name});`, fn);
c.addFunctionCode(`_setThreshold(${fn.args?.[1]?.name});`, fn);
break;
case 'MultisigWeighted':
c.addFunctionCode(`_addSigners(${fn.args?.[0]?.name});`, fn);
c.addFunctionCode(`_setSignerWeights(${fn.args?.[0]?.name}, ${fn.args?.[1]?.name});`, fn);
c.addFunctionCode(`_setThreshold(${fn.args?.[2]?.name});`, fn);
break;
case 'ECDSA':
case 'P256':
case 'RSA':
c.addFunctionCode(
`_setSigner(${fn.args
.map(({ name }) => name)
.join(', ')
.trimEnd()});`,
fn,
);
}
}

const parents = {
Expand All @@ -45,6 +60,14 @@ const parents = {
name: 'SignerRSA',
path: '@openzeppelin/community-contracts/contracts/utils/cryptography/SignerRSA.sol',
},
Multisig: {
name: 'MultiSignerERC7913',
path: '@openzeppelin/community-contracts/contracts/utils/cryptography/MultiSignerERC7913.sol',
},
MultisigWeighted: {
name: 'MultiSignerERC7913Weighted',
path: '@openzeppelin/community-contracts/contracts/utils/cryptography/MultiSignerERC7913Weighted.sol',
},
};

export const signerFunctions = {
Expand All @@ -67,6 +90,21 @@ export const signerFunctions = {
{ name: 'n', type: 'bytes memory' },
],
},
initializeMultisig: {
kind: 'public' as const,
args: [
{ name: 'signers', type: 'bytes[] memory' },
{ name: 'threshold', type: 'uint256' },
],
},
initializeMultisigWeighted: {
kind: 'public' as const,
args: [
{ name: 'signers', type: 'bytes[] memory' },
{ name: 'weights', type: 'uint256[] memory' },
{ name: 'threshold', type: 'uint256' },
],
},
_rawSignatureValidation: {
kind: 'internal' as const,
args: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export const solidityAccountAIFunctionDefinition = {
signer: {
anyOf: [
{ type: 'boolean', enum: [false] },
{ type: 'string', enum: ['ECDSA', 'ERC7702', 'P256', 'RSA'] },
{ type: 'string', enum: ['ECDSA', 'ERC7702', 'P256', 'RSA', 'Multisig', 'MultisigWeighted'] },
],
description: 'Defines the signature verification algorithm used by the account to verify user operations.',
},
Expand Down
23 changes: 23 additions & 0 deletions packages/ui/src/solidity/AccountControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,29 @@
rights to the account while maintaining their native signature verification.
</HelpTooltip>
</label>
<label class:checked={opts.signer === 'Multisig'}>
<input type="radio" bind:group={opts.signer} value="Multisig" />
Multisig
<HelpTooltip link="https://docs.openzeppelin.com/community-contracts/multisig-account#multisignererc7913">
Implementation of ERC-7913 multisignature validation that requires a minimum number of signatures (threshold) to
approve operations. Each signer is encoded as `verifier || key` and must be ordered by their keccak256 hash to
prevent duplicates. The contract maintains a set of authorized signers and validates that the number of valid
signatures meets the threshold requirement.
</HelpTooltip>
</label>
<label class:checked={opts.signer === 'MultisigWeighted'}>
<input type="radio" bind:group={opts.signer} value="MultisigWeighted" />
Multisig Weighted
<HelpTooltip
link="https://docs.openzeppelin.com/community-contracts/0.0.1/multisig-account#multisignererc7913weighted"
>
Extension of ERC-7913 multisignature validation that supports weighted signatures. Each signer can be assigned a
different weight, enabling flexible governance schemes where some signers have more influence than others. The
contract maintains a set of authorized signers and validates that the total weight of valid signatures meets the
threshold requirement. Signers are encoded as `verifier || key` and must be ordered by their keccak256 hash to
prevent duplicates.
</HelpTooltip>
</label>
<label class:checked={opts.signer === 'P256'}>
<input type="radio" bind:group={opts.signer} value="P256" />
P256
Expand Down
9 changes: 1 addition & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==

"@axelar-network/axelar-gmp-sdk-solidity@^6.0.6":
version "6.0.6"
resolved "https://registry.yarnpkg.com/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-6.0.6.tgz#6559394df9b551978ca8843a9cdf3dc89eacdcd4"
integrity sha512-XIcDlr1HoYSqcxuvvusILmiqerh2bL9NJLwU4lFBAJK5E/st/q3Em9ropBBZML9iuUZ+hDsch8Ev9rMO+ulaSQ==

"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
Expand Down Expand Up @@ -570,9 +565,7 @@

"@openzeppelin/community-contracts@https://github.com/OpenZeppelin/openzeppelin-community-contracts":
version "0.0.1"
resolved "https://github.com/OpenZeppelin/openzeppelin-community-contracts#30ed769d7b65723ebe1194398f2b644b6679fb98"
dependencies:
"@axelar-network/axelar-gmp-sdk-solidity" "^6.0.6"
resolved "https://github.com/OpenZeppelin/openzeppelin-community-contracts#2b156fb232f07c6bf304f3988b9b256c75003043"

"@openzeppelin/contracts-upgradeable@^5.3.0":
version "5.3.0"
Expand Down
Loading