Skip to content

Commit d0d1556

Browse files
jeffsmale90ccharly
andauthored
feat: add signEip7702Authorization to KeyringController (#5301)
EIP-7702 defines a new struct Authorization which represents authority to set a pointer to a contract address at an EOA - effectively making the EOA perform as a smart contract. This change integrates the new `signEip7702Authorization` method added to eth- simple and hd keyrings in MetaMask/accounts#182. This is exposed via `KeyringController.signEip7702Authorization` as well as the message handler `KeyringController:SignEip7702Authorization`. See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md for details --------- Co-authored-by: Charly Chevalier <[email protected]>
1 parent 6eaaf7b commit d0d1556

File tree

43 files changed

+300
-120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+300
-120
lines changed

examples/example-controllers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
},
4949
"dependencies": {
5050
"@metamask/base-controller": "^8.0.0",
51-
"@metamask/utils": "^11.1.0"
51+
"@metamask/utils": "^11.2.0"
5252
},
5353
"devDependencies": {
5454
"@metamask/auto-changelog": "^3.4.4",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@metamask/eth-block-tracker": "^11.0.3",
6363
"@metamask/eth-json-rpc-provider": "^4.1.8",
6464
"@metamask/json-rpc-engine": "^10.0.3",
65-
"@metamask/utils": "^11.1.0",
65+
"@metamask/utils": "^11.2.0",
6666
"@ts-bridge/cli": "^0.6.1",
6767
"@types/jest": "^27.4.1",
6868
"@types/lodash": "^4.14.191",

packages/accounts-controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"@metamask/network-controller": "^22.2.1",
5656
"@metamask/snaps-sdk": "^6.17.1",
5757
"@metamask/snaps-utils": "^8.10.0",
58-
"@metamask/utils": "^11.1.0",
58+
"@metamask/utils": "^11.2.0",
5959
"deepmerge": "^4.2.2",
6060
"ethereum-cryptography": "^2.1.2",
6161
"immer": "^9.0.6",

packages/address-book-controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"dependencies": {
5050
"@metamask/base-controller": "^8.0.0",
5151
"@metamask/controller-utils": "^11.5.0",
52-
"@metamask/utils": "^11.1.0"
52+
"@metamask/utils": "^11.2.0"
5353
},
5454
"devDependencies": {
5555
"@metamask/auto-changelog": "^3.4.4",

packages/approval-controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"dependencies": {
5050
"@metamask/base-controller": "^8.0.0",
5151
"@metamask/rpc-errors": "^7.0.2",
52-
"@metamask/utils": "^11.1.0",
52+
"@metamask/utils": "^11.2.0",
5353
"nanoid": "^3.3.8"
5454
},
5555
"devDependencies": {

packages/assets-controllers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"@metamask/polling-controller": "^12.0.3",
6464
"@metamask/rpc-errors": "^7.0.2",
6565
"@metamask/snaps-utils": "^8.10.0",
66-
"@metamask/utils": "^11.1.0",
66+
"@metamask/utils": "^11.2.0",
6767
"@types/bn.js": "^5.1.5",
6868
"@types/uuid": "^8.3.0",
6969
"async-mutex": "^0.5.0",

packages/base-controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
4747
},
4848
"dependencies": {
49-
"@metamask/utils": "^11.1.0",
49+
"@metamask/utils": "^11.2.0",
5050
"immer": "^9.0.6"
5151
},
5252
"devDependencies": {

packages/bridge-controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"@metamask/controller-utils": "^11.5.0",
5252
"@metamask/metamask-eth-abis": "^3.1.1",
5353
"@metamask/polling-controller": "^12.0.3",
54-
"@metamask/utils": "^11.1.0",
54+
"@metamask/utils": "^11.2.0",
5555
"ethers": "^6.12.0"
5656
},
5757
"devDependencies": {

packages/build-utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
4848
},
4949
"dependencies": {
50-
"@metamask/utils": "^11.1.0",
50+
"@metamask/utils": "^11.2.0",
5151
"@types/eslint": "^8.44.7"
5252
},
5353
"devDependencies": {

packages/controller-utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@ethereumjs/util": "^8.1.0",
5151
"@metamask/eth-query": "^4.0.0",
5252
"@metamask/ethjs-unit": "^0.3.0",
53-
"@metamask/utils": "^11.1.0",
53+
"@metamask/utils": "^11.2.0",
5454
"@spruceid/siwe-parser": "2.1.0",
5555
"@types/bn.js": "^5.1.5",
5656
"bignumber.js": "^9.1.2",

packages/ens-controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@ethersproject/providers": "^5.7.0",
5151
"@metamask/base-controller": "^8.0.0",
5252
"@metamask/controller-utils": "^11.5.0",
53-
"@metamask/utils": "^11.1.0",
53+
"@metamask/utils": "^11.2.0",
5454
"punycode": "^2.1.1"
5555
},
5656
"devDependencies": {

packages/eth-json-rpc-provider/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"@metamask/json-rpc-engine": "^10.0.3",
5656
"@metamask/rpc-errors": "^7.0.2",
5757
"@metamask/safe-event-emitter": "^3.0.0",
58-
"@metamask/utils": "^11.1.0",
58+
"@metamask/utils": "^11.2.0",
5959
"uuid": "^8.3.2"
6060
},
6161
"devDependencies": {

packages/gas-fee-controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@metamask/eth-query": "^4.0.0",
5353
"@metamask/ethjs-unit": "^0.3.0",
5454
"@metamask/polling-controller": "^12.0.3",
55-
"@metamask/utils": "^11.1.0",
55+
"@metamask/utils": "^11.2.0",
5656
"@types/bn.js": "^5.1.5",
5757
"@types/uuid": "^8.3.0",
5858
"bn.js": "^5.2.1",

packages/json-rpc-engine/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"dependencies": {
5959
"@metamask/rpc-errors": "^7.0.2",
6060
"@metamask/safe-event-emitter": "^3.0.0",
61-
"@metamask/utils": "^11.1.0"
61+
"@metamask/utils": "^11.2.0"
6262
},
6363
"devDependencies": {
6464
"@lavamoat/allow-scripts": "^3.0.4",

packages/json-rpc-middleware-stream/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"dependencies": {
5050
"@metamask/json-rpc-engine": "^10.0.3",
5151
"@metamask/safe-event-emitter": "^3.0.0",
52-
"@metamask/utils": "^11.1.0",
52+
"@metamask/utils": "^11.2.0",
5353
"readable-stream": "^3.6.2"
5454
},
5555
"devDependencies": {

packages/keyring-controller/package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,12 @@
5151
"@keystonehq/metamask-airgapped-keyring": "^0.14.1",
5252
"@metamask/base-controller": "^8.0.0",
5353
"@metamask/browser-passworder": "^4.3.0",
54-
"@metamask/eth-hd-keyring": "^7.0.4",
55-
"@metamask/eth-sig-util": "^8.0.0",
56-
"@metamask/eth-simple-keyring": "^6.0.5",
54+
"@metamask/eth-hd-keyring": "^10.0.0",
55+
"@metamask/eth-sig-util": "^8.2.0",
56+
"@metamask/eth-simple-keyring": "^8.1.0",
5757
"@metamask/keyring-api": "^17.0.0",
5858
"@metamask/keyring-internal-api": "^4.0.1",
59-
"@metamask/message-manager": "^12.0.1",
60-
"@metamask/utils": "^11.1.0",
59+
"@metamask/utils": "^11.2.0",
6160
"async-mutex": "^0.5.0",
6261
"ethereumjs-wallet": "^1.0.1",
6362
"immer": "^9.0.6",

packages/keyring-controller/src/KeyringController.test.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import {
1010
recoverTypedSignature,
1111
SignTypedDataVersion,
1212
encrypt,
13+
recoverEIP7702Authorization,
1314
} from '@metamask/eth-sig-util';
14-
import SimpleKeyring from '@metamask/eth-simple-keyring/dist/simple-keyring';
15+
import SimpleKeyring from '@metamask/eth-simple-keyring';
1516
import type { EthKeyring } from '@metamask/keyring-internal-api';
1617
import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english';
1718
import type { KeyringClass } from '@metamask/utils';
@@ -105,7 +106,8 @@ describe('KeyringController', () => {
105106

106107
it('allows overwriting the built-in Simple keyring builder', async () => {
107108
const mockSimpleKeyringBuilder =
108-
// @ts-expect-error The simple keyring doesn't yet conform to the KeyringClass type
109+
// todo: keyring types are mismatched, this should be fixed in they keyrings themselves
110+
// @ts-expect-error keyring types are mismatched
109111
buildKeyringBuilderWithSpy(SimpleKeyring);
110112
await withController(
111113
{ keyringBuilders: [mockSimpleKeyringBuilder] },
@@ -118,6 +120,8 @@ describe('KeyringController', () => {
118120
});
119121

120122
it('allows overwriting the built-in HD keyring builder', async () => {
123+
// todo: keyring types are mismatched, this should be fixed in they keyrings themselves
124+
// @ts-expect-error keyring types are mismatched
121125
const mockHdKeyringBuilder = buildKeyringBuilderWithSpy(HDKeyring);
122126
await withController(
123127
{ keyringBuilders: [mockHdKeyringBuilder] },
@@ -621,6 +625,8 @@ describe('KeyringController', () => {
621625
it('should throw error if the first account is not found on the keyring', async () => {
622626
jest
623627
.spyOn(HDKeyring.prototype, 'getAccounts')
628+
// todo: keyring types are mismatched, this should be fixed in they keyrings themselves
629+
// @ts-expect-error keyring types are mismatched
624630
.mockResolvedValue([]);
625631
await withController(
626632
{ cacheEncryptionKey, skipVaultCreation: true },
@@ -1672,6 +1678,74 @@ describe('KeyringController', () => {
16721678
});
16731679
});
16741680

1681+
describe('signEip7702Authorization', () => {
1682+
const from = '0x5AC6D462f054690a373FABF8CC28e161003aEB19';
1683+
stubKeyringClassWithAccount(MockKeyring, from);
1684+
const chainId = 1;
1685+
const contractAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F';
1686+
const nonce = 1;
1687+
1688+
describe('when the keyring for the given address supports signEip7702Authorization', () => {
1689+
it('should sign EIP-7702 authorization message', async () => {
1690+
await withController(async ({ controller, initialState }) => {
1691+
const account = initialState.keyrings[0].accounts[0];
1692+
const signature = await controller.signEip7702Authorization({
1693+
from: account,
1694+
chainId,
1695+
contractAddress,
1696+
nonce,
1697+
});
1698+
1699+
const recovered = recoverEIP7702Authorization({
1700+
authorization: [chainId, contractAddress, nonce],
1701+
signature,
1702+
});
1703+
1704+
expect(recovered).toBe(account);
1705+
});
1706+
});
1707+
1708+
it('should not sign EIP-7702 authorization message if from account is not passed', async () => {
1709+
await withController(async ({ controller }) => {
1710+
await expect(
1711+
controller.signEip7702Authorization({
1712+
chainId,
1713+
contractAddress,
1714+
nonce,
1715+
from: '',
1716+
}),
1717+
).rejects.toThrow(
1718+
'KeyringController - No keyring found. Error info: There are keyrings, but none match the address',
1719+
);
1720+
});
1721+
});
1722+
});
1723+
1724+
describe('when the keyring for the given address does not support signEip7702Authorization', () => {
1725+
it('should throw error', async () => {
1726+
stubKeyringClassWithAccount(MockKeyring, from);
1727+
1728+
await withController(
1729+
{ keyringBuilders: [keyringBuilderFactory(MockKeyring)] },
1730+
async ({ controller }) => {
1731+
await controller.addNewKeyring(MockKeyring.type);
1732+
1733+
await expect(
1734+
controller.signEip7702Authorization({
1735+
from,
1736+
chainId,
1737+
contractAddress,
1738+
nonce,
1739+
}),
1740+
).rejects.toThrow(
1741+
KeyringControllerError.UnsupportedSignEip7702Authorization,
1742+
);
1743+
},
1744+
);
1745+
});
1746+
});
1747+
});
1748+
16751749
describe('signTypedMessage', () => {
16761750
describe('when the keyring for the given address supports signTypedMessage', () => {
16771751
it('should throw when given invalid version', async () => {

packages/keyring-controller/src/KeyringController.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ import type {
1818
EthUserOperationPatch,
1919
} from '@metamask/keyring-api';
2020
import type { EthKeyring } from '@metamask/keyring-internal-api';
21-
import type {
22-
PersonalMessageParams,
23-
TypedMessageParams,
24-
} from '@metamask/message-manager';
2521
import type {
2622
Eip1024EncryptedData,
2723
Hex,
@@ -47,6 +43,11 @@ import type { Patch } from 'immer';
4743
import { ulid } from 'ulid';
4844

4945
import { KeyringControllerError } from './constants';
46+
import type {
47+
Eip7702AuthorizationParams,
48+
PersonalMessageParams,
49+
TypedMessageParams,
50+
} from './types';
5051

5152
const name = 'KeyringController';
5253

@@ -123,6 +124,11 @@ export type KeyringControllerSignMessageAction = {
123124
handler: KeyringController['signMessage'];
124125
};
125126

127+
export type KeyringControllerSignEip7702AuthorizationAction = {
128+
type: `${typeof name}:signEip7702Authorization`;
129+
handler: KeyringController['signEip7702Authorization'];
130+
};
131+
126132
export type KeyringControllerSignPersonalMessageAction = {
127133
type: `${typeof name}:signPersonalMessage`;
128134
handler: KeyringController['signPersonalMessage'];
@@ -216,6 +222,7 @@ export type KeyringControllerQRKeyringStateChangeEvent = {
216222
export type KeyringControllerActions =
217223
| KeyringControllerGetStateAction
218224
| KeyringControllerSignMessageAction
225+
| KeyringControllerSignEip7702AuthorizationAction
219226
| KeyringControllerSignPersonalMessageAction
220227
| KeyringControllerSignTypedMessageAction
221228
| KeyringControllerDecryptMessageAction
@@ -452,7 +459,10 @@ export function keyringBuilderFactory(KeyringConstructor: KeyringClass<Json>) {
452459
}
453460

454461
const defaultKeyringBuilders = [
462+
// todo: keyring types are mismatched, this should be fixed in they keyrings themselves
463+
// @ts-expect-error keyring types are mismatched
455464
keyringBuilderFactory(SimpleKeyring),
465+
// @ts-expect-error keyring types are mismatched
456466
keyringBuilderFactory(HDKeyring),
457467
];
458468

@@ -1182,6 +1192,38 @@ export class KeyringController extends BaseController<
11821192
return await keyring.signMessage(address, messageParams.data);
11831193
}
11841194

1195+
/**
1196+
* Signs EIP-7702 Authorization message by calling down into a specific keyring.
1197+
*
1198+
* @param params - EIP7702AuthorizationParams object to sign.
1199+
* @returns Promise resolving to an EIP-7702 Authorization signature.
1200+
* @throws Will throw UnsupportedSignEIP7702Authorization if the keyring does not support signing EIP-7702 Authorization messages.
1201+
*/
1202+
async signEip7702Authorization(
1203+
params: Eip7702AuthorizationParams,
1204+
): Promise<string> {
1205+
const from = ethNormalize(params.from) as Hex;
1206+
1207+
const keyring = (await this.getKeyringForAccount(from)) as EthKeyring<Json>;
1208+
1209+
if (!keyring.signEip7702Authorization) {
1210+
throw new Error(
1211+
KeyringControllerError.UnsupportedSignEip7702Authorization,
1212+
);
1213+
}
1214+
1215+
const { chainId, nonce } = params;
1216+
const contractAddress = ethNormalize(params.contractAddress) as
1217+
| Hex
1218+
| undefined;
1219+
1220+
return await keyring.signEip7702Authorization(from, [
1221+
chainId,
1222+
contractAddress as Hex,
1223+
nonce,
1224+
]);
1225+
}
1226+
11851227
/**
11861228
* Signs personal message by calling down into a specific keyring.
11871229
*
@@ -1795,6 +1837,11 @@ export class KeyringController extends BaseController<
17951837
this.signMessage.bind(this),
17961838
);
17971839

1840+
this.messagingSystem.registerActionHandler(
1841+
`${name}:signEip7702Authorization`,
1842+
this.signEip7702Authorization.bind(this),
1843+
);
1844+
17981845
this.messagingSystem.registerActionHandler(
17991846
`${name}:signPersonalMessage`,
18001847
this.signPersonalMessage.bind(this),

packages/keyring-controller/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export enum KeyringControllerError {
1515
UnsupportedSignTransaction = 'KeyringController - The keyring for the current address does not support the method signTransaction.',
1616
UnsupportedSignMessage = 'KeyringController - The keyring for the current address does not support the method signMessage.',
1717
UnsupportedSignPersonalMessage = 'KeyringController - The keyring for the current address does not support the method signPersonalMessage.',
18+
UnsupportedSignEip7702Authorization = 'KeyringController - The keyring for the current address does not support the method signEip7702Authorization.',
1819
UnsupportedGetEncryptionPublicKey = 'KeyringController - The keyring for the current address does not support the method getEncryptionPublicKey.',
1920
UnsupportedDecryptMessage = 'KeyringController - The keyring for the current address does not support the method decryptMessage.',
2021
UnsupportedSignTypedMessage = 'KeyringController - The keyring for the current address does not support the method signTypedMessage.',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './KeyringController';
2+
export type * from './types';

0 commit comments

Comments
 (0)