From c8ef6e0858149614ef61f80481799d183dcd9dc0 Mon Sep 17 00:00:00 2001 From: John Whiles Date: Tue, 28 Apr 2026 16:43:35 +0200 Subject: [PATCH 1/3] feat: add build delegations step to money upgrade --- .../jest.config.js | 18 +- .../package.json | 6 +- .../src/MoneyAccountUpgradeController.ts | 26 +- .../src/steps/build-delegations.test.ts | 312 ++++++++++++++++++ .../src/steps/build-delegations.ts | 110 ++++++ .../src/steps/step.ts | 2 + .../src/types.ts | 1 + .../tsconfig.build.json | 3 +- .../tsconfig.json | 3 +- yarn.lock | 132 +++++++- 10 files changed, 598 insertions(+), 15 deletions(-) create mode 100644 packages/money-account-upgrade-controller/src/steps/build-delegations.test.ts create mode 100644 packages/money-account-upgrade-controller/src/steps/build-delegations.ts diff --git a/packages/money-account-upgrade-controller/jest.config.js b/packages/money-account-upgrade-controller/jest.config.js index ca084133399..0eb05a8befb 100644 --- a/packages/money-account-upgrade-controller/jest.config.js +++ b/packages/money-account-upgrade-controller/jest.config.js @@ -3,18 +3,15 @@ * https://jestjs.io/docs/configuration */ -const merge = require('deepmerge'); const path = require('path'); const baseConfig = require('../../jest.config.packages'); const displayName = path.basename(__dirname); -module.exports = merge(baseConfig, { - // The display name when running multiple projects +module.exports = { + ...baseConfig, displayName, - - // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { branches: 100, @@ -23,4 +20,13 @@ module.exports = merge(baseConfig, { statements: 100, }, }, -}); + // The base config's `^@metamask/(.+)$` mapper rewrites every `@metamask/*` + // import without honouring the package.json `exports` field, which breaks + // subpath imports like `@metamask/smart-accounts-kit/utils`. Resolve those + // explicitly here, before falling through to the base mapper. + moduleNameMapper: { + '^@metamask/smart-accounts-kit/utils$': + require.resolve('@metamask/smart-accounts-kit/utils'), + ...baseConfig.moduleNameMapper, + }, +}; diff --git a/packages/money-account-upgrade-controller/package.json b/packages/money-account-upgrade-controller/package.json index 050a9101881..a7a7137123e 100644 --- a/packages/money-account-upgrade-controller/package.json +++ b/packages/money-account-upgrade-controller/package.json @@ -53,12 +53,16 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { + "@metamask/authenticated-user-storage": "^1.0.0", "@metamask/base-controller": "^9.1.0", "@metamask/chomp-api-service": "^3.0.0", "@metamask/keyring-controller": "^25.3.0", "@metamask/messenger": "^1.2.0", "@metamask/network-controller": "^30.1.0", - "@metamask/utils": "^11.9.0" + "@metamask/smart-accounts-kit": "^1.3.0", + "@metamask/utils": "^11.9.0", + "uuid": "^8.3.2", + "viem": "^2.46.2" }, "devDependencies": { "@metamask/auto-changelog": "^6.1.0", diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index 113fca42607..a62abc12e33 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -1,3 +1,4 @@ +import type { AuthenticatedUserStorageServiceListDelegationsAction } from '@metamask/authenticated-user-storage'; import type { ControllerGetStateAction, ControllerStateChangedEvent, @@ -8,10 +9,12 @@ import type { ChompApiServiceAssociateAddressAction, ChompApiServiceCreateUpgradeAction, ChompApiServiceGetServiceDetailsAction, + ChompApiServiceVerifyDelegationAction, } from '@metamask/chomp-api-service'; import type { KeyringControllerSignEip7702AuthorizationAction, KeyringControllerSignPersonalMessageAction, + KeyringControllerSignTypedMessageAction, } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { @@ -22,6 +25,7 @@ import type { Hex } from '@metamask/utils'; import type { MoneyAccountUpgradeControllerMethodActions } from './MoneyAccountUpgradeController-method-action-types'; import { associateAddressStep } from './steps/associate-address'; +import { buildDelegationStep } from './steps/build-delegations'; import { eip7702AuthorizationStep } from './steps/eip-7702-authorization'; import type { Step } from './steps/step'; import type { InitConfig } from './types'; @@ -49,10 +53,13 @@ type AllowedActions = | ChompApiServiceAssociateAddressAction | ChompApiServiceCreateUpgradeAction | ChompApiServiceGetServiceDetailsAction + | ChompApiServiceVerifyDelegationAction | KeyringControllerSignEip7702AuthorizationAction | KeyringControllerSignPersonalMessageAction + | KeyringControllerSignTypedMessageAction | NetworkControllerFindNetworkClientIdByChainIdAction - | NetworkControllerGetNetworkClientByIdAction; + | NetworkControllerGetNetworkClientByIdAction + | AuthenticatedUserStorageServiceListDelegationsAction; export type MoneyAccountUpgradeControllerStateChangedEvent = ControllerStateChangedEvent< @@ -79,9 +86,18 @@ export class MoneyAccountUpgradeController extends BaseController< MoneyAccountUpgradeControllerState, MoneyAccountUpgradeControllerMessenger > { - #config?: { chainId: Hex; delegatorImplAddress: Hex }; - - readonly #steps: Step[] = [associateAddressStep, eip7702AuthorizationStep]; + #config?: { + chainId: Hex; + delegatorImplAddress: Hex; + tokenAddress: Hex; + redeemerAddress: Hex; + }; + + readonly #steps: Step[] = [ + associateAddressStep, + eip7702AuthorizationStep, + buildDelegationStep, + ]; /** * Constructor for the MoneyAccountUpgradeController. @@ -141,6 +157,8 @@ export class MoneyAccountUpgradeController extends BaseController< this.#config = { chainId, delegatorImplAddress: initConfig.delegatorImplAddress, + tokenAddress: initConfig.musdTokenAddress, + redeemerAddress: initConfig.vedaVaultAdapterAddress, }; } diff --git a/packages/money-account-upgrade-controller/src/steps/build-delegations.test.ts b/packages/money-account-upgrade-controller/src/steps/build-delegations.test.ts new file mode 100644 index 00000000000..ad8280b16c3 --- /dev/null +++ b/packages/money-account-upgrade-controller/src/steps/build-delegations.test.ts @@ -0,0 +1,312 @@ +import { SignTypedDataVersion } from '@metamask/keyring-controller'; +import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; +import type { + MockAnyNamespace, + MessengerActions, + MessengerEvents, +} from '@metamask/messenger'; +import { + createDelegation, + getSmartAccountsEnvironment, +} from '@metamask/smart-accounts-kit'; +import { + hashDelegation, + toDelegationStruct, +} from '@metamask/smart-accounts-kit/utils'; +import type { Hex } from '@metamask/utils'; + +import type { MoneyAccountUpgradeControllerMessenger } from '../MoneyAccountUpgradeController'; +import { + buildDelegationsSteps, + SIGNABLE_DELEGATION_TYPED_DATA, +} from './build-delegations'; + +jest.mock('@metamask/smart-accounts-kit', () => ({ + createDelegation: jest.fn(), + getSmartAccountsEnvironment: jest.fn(), +})); + +jest.mock( + '@metamask/smart-accounts-kit/utils', + () => ({ + hashDelegation: jest.fn(), + toDelegationStruct: jest.fn(), + }), + { virtual: true }, +); + +const mockCreateDelegation = jest.mocked(createDelegation); +const mockGetEnvironment = jest.mocked(getSmartAccountsEnvironment); +const mockHashDelegation = jest.mocked(hashDelegation); +const mockToDelegationStruct = jest.mocked(toDelegationStruct); + +const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; +const MOCK_CHAIN_ID = '0xaa36a7' as Hex; // 11155111 (Sepolia) +const MOCK_CHAIN_ID_DECIMAL = 11155111; +const MOCK_DELEGATOR_IMPL = '0x2222222222222222222222222222222222222222' as Hex; +const MOCK_TOKEN = '0x3333333333333333333333333333333333333333' as Hex; +const MOCK_REDEEMER = '0x4444444444444444444444444444444444444444' as Hex; +const MOCK_DELEGATION_MANAGER = + '0x5555555555555555555555555555555555555555' as Hex; +const MOCK_HASH: Hex = `0x${'ab'.repeat(32)}`; +const MOCK_SIGNATURE: Hex = `0x${'cd'.repeat(65)}`; +const NON_MATCHING_HASH: Hex = `0x${'ef'.repeat(32)}`; + +const AAVE_ADAPTER_ADDRESS = '0xbab56C2Ea37C5976247eF2dAfBf7FC4B97e7Af1c'; +const MAX_UINT256 = + '115792089237316195423570985008687907853269984665640564039457584007913129639935'; + +const MOCK_ENVIRONMENT = { + DelegationManager: MOCK_DELEGATION_MANAGER, +} as ReturnType; + +const MOCK_DELEGATION: ReturnType = { + delegate: MOCK_REDEEMER, + delegator: MOCK_ADDRESS, + authority: + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex, + caveats: [ + { + enforcer: '0x6666666666666666666666666666666666666666' as Hex, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + ], + salt: `0x${'42'.repeat(32)}`, + signature: '0x' as Hex, +}; + +const MOCK_DELEGATION_STRUCT = { + ...MOCK_DELEGATION, + salt: BigInt(MOCK_DELEGATION.salt), +}; + +type AllActions = MessengerActions; +type AllEvents = MessengerEvents; + +type Mocks = { + listDelegations: jest.Mock; + signTypedMessage: jest.Mock; + verifyDelegation: jest.Mock; +}; + +function setup(): { + messenger: MoneyAccountUpgradeControllerMessenger; + mocks: Mocks; +} { + const mocks: Mocks = { + listDelegations: jest.fn().mockResolvedValue([]), + signTypedMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE), + verifyDelegation: jest.fn().mockResolvedValue({ valid: true }), + }; + + const rootMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); + rootMessenger.registerActionHandler( + 'AuthenticatedUserStorageService:listDelegations', + mocks.listDelegations, + ); + rootMessenger.registerActionHandler( + 'KeyringController:signTypedMessage', + mocks.signTypedMessage, + ); + rootMessenger.registerActionHandler( + 'ChompApiService:verifyDelegation', + mocks.verifyDelegation, + ); + + const messenger: MoneyAccountUpgradeControllerMessenger = new Messenger({ + namespace: 'MoneyAccountUpgradeController', + parent: rootMessenger, + }); + rootMessenger.delegate({ + actions: [ + 'AuthenticatedUserStorageService:listDelegations', + 'KeyringController:signTypedMessage', + 'ChompApiService:verifyDelegation', + ], + events: [], + messenger, + }); + + return { messenger, mocks }; +} + +async function run( + messenger: MoneyAccountUpgradeControllerMessenger, +): ReturnType { + return buildDelegationsSteps.run({ + messenger, + address: MOCK_ADDRESS, + chainId: MOCK_CHAIN_ID, + delegatorImplAddress: MOCK_DELEGATOR_IMPL, + tokenAddress: MOCK_TOKEN, + redeemerAddress: MOCK_REDEEMER, + }); +} + +describe('buildDelegationsSteps', () => { + beforeEach(() => { + mockGetEnvironment.mockReturnValue(MOCK_ENVIRONMENT); + mockCreateDelegation.mockReturnValue(MOCK_DELEGATION); + mockHashDelegation.mockReturnValue(MOCK_HASH); + mockToDelegationStruct.mockReturnValue(MOCK_DELEGATION_STRUCT); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('is named "build-delegations"', () => { + expect(buildDelegationsSteps.name).toBe('build-delegations'); + }); + + it('builds the delegation against the chain-specific environment using a fresh 32-byte salt', async () => { + const { messenger } = setup(); + + await run(messenger); + + expect(mockGetEnvironment).toHaveBeenCalledWith(MOCK_CHAIN_ID_DECIMAL); + expect(mockCreateDelegation).toHaveBeenCalledWith({ + environment: MOCK_ENVIRONMENT, + scope: { + type: 'erc20TransferAmount', + tokenAddress: MOCK_TOKEN, + maxAmount: BigInt(MAX_UINT256), + }, + from: MOCK_ADDRESS, + to: MOCK_REDEEMER, + caveats: [ + { + type: 'redeemer', + redeemers: [AAVE_ADAPTER_ADDRESS], + }, + ], + // 32-byte 0x-prefixed hex string. + salt: expect.stringMatching(/^0x[0-9a-f]{64}$/u) as Hex, + }); + }); + + describe('when an existing delegation has the same hash', () => { + it('returns "already-done" without signing or submitting', async () => { + const { messenger, mocks } = setup(); + mocks.listDelegations.mockResolvedValue([ + { + signedDelegation: { ...MOCK_DELEGATION, signature: '0x' }, + metadata: { + delegationHash: MOCK_HASH, + chainIdHex: MOCK_CHAIN_ID, + allowance: '0x00', + tokenSymbol: 'mUSD', + tokenAddress: MOCK_TOKEN, + type: 'lend', + }, + }, + ]); + + const result = await run(messenger); + + expect(result).toBe('already-done'); + expect(mocks.signTypedMessage).not.toHaveBeenCalled(); + expect(mocks.verifyDelegation).not.toHaveBeenCalled(); + }); + + it('ignores entries with non-matching hashes', async () => { + const { messenger, mocks } = setup(); + mocks.listDelegations.mockResolvedValue([ + { + signedDelegation: { ...MOCK_DELEGATION, signature: '0x' }, + metadata: { + delegationHash: NON_MATCHING_HASH, + chainIdHex: MOCK_CHAIN_ID, + allowance: '0x00', + tokenSymbol: 'mUSD', + tokenAddress: MOCK_TOKEN, + type: 'lend', + }, + }, + ]); + + const result = await run(messenger); + + expect(result).toBe('completed'); + expect(mocks.signTypedMessage).toHaveBeenCalledTimes(1); + expect(mocks.verifyDelegation).toHaveBeenCalledTimes(1); + }); + }); + + describe('when no existing delegation matches', () => { + it('signs the delegation as EIP-712 V4 typed data scoped to the DelegationManager', async () => { + const { messenger, mocks } = setup(); + + await run(messenger); + + expect(mocks.signTypedMessage).toHaveBeenCalledWith( + { + from: MOCK_ADDRESS, + data: { + domain: { + name: 'DelegationManager', + version: '1', + chainId: MOCK_CHAIN_ID_DECIMAL, + verifyingContract: MOCK_DELEGATION_MANAGER, + }, + types: SIGNABLE_DELEGATION_TYPED_DATA, + primaryType: 'Delegation', + message: MOCK_DELEGATION_STRUCT, + }, + }, + SignTypedDataVersion.V4, + ); + }); + + it('submits the signed delegation to ChompApiService:verifyDelegation', async () => { + const { messenger, mocks } = setup(); + + await run(messenger); + + expect(mocks.verifyDelegation).toHaveBeenCalledWith({ + signedDelegation: { + ...MOCK_DELEGATION, + signature: MOCK_SIGNATURE, + }, + chainId: MOCK_CHAIN_ID, + }); + }); + + it('returns "completed" on success', async () => { + const { messenger } = setup(); + + const result = await run(messenger); + + expect(result).toBe('completed'); + }); + }); + + describe('error propagation', () => { + it('propagates errors from listDelegations and does not sign or submit', async () => { + const { messenger, mocks } = setup(); + mocks.listDelegations.mockRejectedValue(new Error('storage failed')); + + await expect(run(messenger)).rejects.toThrow('storage failed'); + expect(mocks.signTypedMessage).not.toHaveBeenCalled(); + expect(mocks.verifyDelegation).not.toHaveBeenCalled(); + }); + + it('propagates errors from signing and does not submit to CHOMP', async () => { + const { messenger, mocks } = setup(); + mocks.signTypedMessage.mockRejectedValue(new Error('signing failed')); + + await expect(run(messenger)).rejects.toThrow('signing failed'); + expect(mocks.verifyDelegation).not.toHaveBeenCalled(); + }); + + it('propagates errors from verifyDelegation', async () => { + const { messenger, mocks } = setup(); + mocks.verifyDelegation.mockRejectedValue(new Error('chomp failed')); + + await expect(run(messenger)).rejects.toThrow('chomp failed'); + }); + }); +}); diff --git a/packages/money-account-upgrade-controller/src/steps/build-delegations.ts b/packages/money-account-upgrade-controller/src/steps/build-delegations.ts new file mode 100644 index 00000000000..6a3ae1beb09 --- /dev/null +++ b/packages/money-account-upgrade-controller/src/steps/build-delegations.ts @@ -0,0 +1,110 @@ +import type { DelegationResponse } from '@metamask/authenticated-user-storage'; +import { SignTypedDataVersion } from '@metamask/keyring-controller'; +import { + createDelegation, + getSmartAccountsEnvironment, +} from '@metamask/smart-accounts-kit'; +import { + hashDelegation, + toDelegationStruct, +} from '@metamask/smart-accounts-kit/utils'; +import { hexToNumber, bytesToHex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; +import { webcrypto } from 'node:crypto'; + +import type { Step } from './step'; + +const MAX_UINT256 = + '115792089237316195423570985008687907853269984665640564039457584007913129639935'; + +// Sourced from https://github.com/MetaMask/snap-cash-account-poc/blob/70709e15ddc56288dd9eefa45b425a756f25d2fb/packages/snap/src/api/config.ts#L39-L40 +const AAVE_ADAPTER_ADDRESS = '0xbab56C2Ea37C5976247eF2dAfBf7FC4B97e7Af1c'; + +/** + * EIP-712 typed data structure for signing delegations. + */ +export const SIGNABLE_DELEGATION_TYPED_DATA = { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Caveat: [ + { name: 'enforcer', type: 'address' }, + { name: 'terms', type: 'bytes' }, + ], + Delegation: [ + { name: 'delegate', type: 'address' }, + { name: 'delegator', type: 'address' }, + { name: 'authority', type: 'bytes32' }, + { name: 'caveats', type: 'Caveat[]' }, + { name: 'salt', type: 'uint256' }, + ], +} as const; + +export const buildDelegationStep: Step = { + name: 'build-delegation', + async run({ messenger, address, redeemerAddress, chainId, tokenAddress }) { + const saltBytes = webcrypto.getRandomValues(new Uint8Array(32)); + const salt = bytesToHex(saltBytes); + const chainIdDecimal = hexToNumber(chainId); + const environment = getSmartAccountsEnvironment(chainIdDecimal); + const delegation = createDelegation({ + environment, + scope: { + type: 'erc20TransferAmount', + tokenAddress, + maxAmount: BigInt(MAX_UINT256), + }, + from: address, + to: redeemerAddress, + caveats: [ + { + type: 'redeemer', + redeemers: [AAVE_ADAPTER_ADDRESS], + }, + ], + salt, + }); + + const delegationHash = hashDelegation(delegation); + + const existingDelegations = await messenger.call( + 'AuthenticatedUserStorageService:listDelegations', + ); + if ( + existingDelegations.some( + (entry: DelegationResponse) => + entry.metadata.delegationHash === delegationHash, + ) + ) { + return 'already-done'; + } + + const typedData = { + domain: { + name: 'DelegationManager', + version: '1', + chainId: chainIdDecimal, + verifyingContract: environment.DelegationManager, + }, + types: SIGNABLE_DELEGATION_TYPED_DATA, + primaryType: 'Delegation' as const, + message: toDelegationStruct({ ...delegation, signature: '0x' }), + }; + + const signature = (await messenger.call( + 'KeyringController:signTypedMessage', + { from: address, data: typedData }, + SignTypedDataVersion.V4, + )) as Hex; + + await messenger.call('ChompApiService:verifyDelegation', { + signedDelegation: { ...delegation, signature }, + chainId, + }); + + return 'completed'; + }, +}; diff --git a/packages/money-account-upgrade-controller/src/steps/step.ts b/packages/money-account-upgrade-controller/src/steps/step.ts index fa164d33543..845d704ec51 100644 --- a/packages/money-account-upgrade-controller/src/steps/step.ts +++ b/packages/money-account-upgrade-controller/src/steps/step.ts @@ -10,6 +10,8 @@ export type StepContext = { address: Hex; chainId: Hex; delegatorImplAddress: Hex; + tokenAddress: Hex; + redeemerAddress: Hex; }; /** diff --git a/packages/money-account-upgrade-controller/src/types.ts b/packages/money-account-upgrade-controller/src/types.ts index c6a18dc179d..abc3661f164 100644 --- a/packages/money-account-upgrade-controller/src/types.ts +++ b/packages/money-account-upgrade-controller/src/types.ts @@ -31,4 +31,5 @@ export type InitConfig = Pick< | 'musdTokenAddress' | 'redeemerEnforcer' | 'valueLteEnforcer' + | 'vedaVaultAdapterAddress' >; diff --git a/packages/money-account-upgrade-controller/tsconfig.build.json b/packages/money-account-upgrade-controller/tsconfig.build.json index b69bb81ccab..61d05b9ec44 100644 --- a/packages/money-account-upgrade-controller/tsconfig.build.json +++ b/packages/money-account-upgrade-controller/tsconfig.build.json @@ -11,7 +11,8 @@ { "path": "../keyring-controller/tsconfig.build.json" }, { "path": "../messenger/tsconfig.build.json" }, { "path": "../network-controller/tsconfig.build.json" }, - { "path": "../transaction-controller/tsconfig.build.json" } + { "path": "../transaction-controller/tsconfig.build.json" }, + { "path": "../authenticated-user-storage/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/money-account-upgrade-controller/tsconfig.json b/packages/money-account-upgrade-controller/tsconfig.json index ffcde5ec67f..8fb9193e79b 100644 --- a/packages/money-account-upgrade-controller/tsconfig.json +++ b/packages/money-account-upgrade-controller/tsconfig.json @@ -9,7 +9,8 @@ { "path": "../keyring-controller" }, { "path": "../messenger" }, { "path": "../network-controller" }, - { "path": "../transaction-controller" } + { "path": "../transaction-controller" }, + { "path": "../authenticated-user-storage" } ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index b727ed5239b..82fb89ad071 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2924,7 +2924,7 @@ __metadata: languageName: node linkType: hard -"@metamask/authenticated-user-storage@workspace:packages/authenticated-user-storage": +"@metamask/authenticated-user-storage@npm:^1.0.0, @metamask/authenticated-user-storage@workspace:packages/authenticated-user-storage": version: 0.0.0-use.local resolution: "@metamask/authenticated-user-storage@workspace:packages/authenticated-user-storage" dependencies: @@ -3457,6 +3457,13 @@ __metadata: languageName: node linkType: hard +"@metamask/delegation-abis@npm:^1.0.0": + version: 1.0.0 + resolution: "@metamask/delegation-abis@npm:1.0.0" + checksum: 10/31b58d3080710cd48fbf471a1cb94fc4cae705f2472d392a3daaa7c73603ad6d0b1c9068e3b71b10f9c39bfe19c1fad90ecf3115e20a00b2d39f5c7789c5b932 + languageName: node + linkType: hard + "@metamask/delegation-controller@workspace:packages/delegation-controller": version: 0.0.0-use.local resolution: "@metamask/delegation-controller@workspace:packages/delegation-controller" @@ -3496,6 +3503,13 @@ __metadata: languageName: node linkType: hard +"@metamask/delegation-deployments@npm:^1.2.0": + version: 1.2.0 + resolution: "@metamask/delegation-deployments@npm:1.2.0" + checksum: 10/f9cd63d05dd9e9627696fd9fea9c32937839fab5e5f4a6ee15a4d32887ec58069aec1626608cfa8b4b5d39f75070c0329347e72d968fa9e3902932da8365d5c1 + languageName: node + linkType: hard + "@metamask/earn-controller@workspace:packages/earn-controller": version: 0.0.0-use.local resolution: "@metamask/earn-controller@workspace:packages/earn-controller" @@ -4516,12 +4530,14 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/money-account-upgrade-controller@workspace:packages/money-account-upgrade-controller" dependencies: + "@metamask/authenticated-user-storage": "npm:^1.0.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/chomp-api-service": "npm:^3.0.0" "@metamask/keyring-controller": "npm:^25.3.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^30.1.0" + "@metamask/smart-accounts-kit": "npm:^1.3.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -4532,6 +4548,8 @@ __metadata: typedoc: "npm:^0.25.13" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.3.3" + uuid: "npm:^8.3.2" + viem: "npm:^2.46.2" languageName: unknown linkType: soft @@ -5403,6 +5421,22 @@ __metadata: languageName: node linkType: hard +"@metamask/smart-accounts-kit@npm:^1.3.0": + version: 1.3.0 + resolution: "@metamask/smart-accounts-kit@npm:1.3.0" + dependencies: + "@metamask/7715-permission-types": "npm:^0.5.0" + "@metamask/delegation-abis": "npm:^1.0.0" + "@metamask/delegation-core": "npm:^1.1.0" + "@metamask/delegation-deployments": "npm:^1.2.0" + openapi-fetch: "npm:^0.13.5" + ox: "npm:0.8.1" + peerDependencies: + viem: ^2.31.4 + checksum: 10/45da6ddafcdef5c3c0247efa1d2a431349b6bd2684b6b92cc69c3f38eab5816385cbb197c52093811b588b80f4b71c57517861c76cd906c000906d814514430d + languageName: node + linkType: hard + "@metamask/snap-account-service@workspace:packages/snap-account-service": version: 0.0.0-use.local resolution: "@metamask/snap-account-service@workspace:packages/snap-account-service" @@ -5889,7 +5923,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:^1.2.0, @noble/curves@npm:^1.8.1, @noble/curves@npm:^1.9.2, @noble/curves@npm:~1.9.0": +"@noble/curves@npm:^1.2.0, @noble/curves@npm:^1.8.1, @noble/curves@npm:^1.9.1, @noble/curves@npm:^1.9.2, @noble/curves@npm:~1.9.0": version: 1.9.7 resolution: "@noble/curves@npm:1.9.7" dependencies: @@ -7423,6 +7457,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:^1.0.8": + version: 1.2.4 + resolution: "abitype@npm:1.2.4" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: 10/500b317a53b34cb6ffe3e4f090e135972b43cd2fbdfebe64fc497dfd8515d9117919e5f88f0aaede332d29a21c1826be64a3ffa620b0b91c16e8b560b6635714 + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -12850,6 +12899,22 @@ __metadata: languageName: node linkType: hard +"openapi-fetch@npm:^0.13.5": + version: 0.13.8 + resolution: "openapi-fetch@npm:0.13.8" + dependencies: + openapi-typescript-helpers: "npm:^0.0.15" + checksum: 10/fed630452ac2d6abc680402651d848b7377b651164ca2be61a8c5e1fc89e41b09c928ba9dc92cf7c7ad2d400b3fbe5af380165303f293501dc08cefa4c0f92fd + languageName: node + linkType: hard + +"openapi-typescript-helpers@npm:^0.0.15": + version: 0.0.15 + resolution: "openapi-typescript-helpers@npm:0.0.15" + checksum: 10/63f8f0b8464aed3e5c6910428bd14839bd5c1dd6ddf841bcea9d5f536a6e03e942a028202920da1a8b1ed9e4304c6fca14943d01a8adff2942d1254a229b8c70 + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -12885,6 +12950,48 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.14.20": + version: 0.14.20 + resolution: "ox@npm:0.14.20" + dependencies: + "@adraffy/ens-normalize": "npm:^1.11.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/curves": "npm:1.9.1" + "@noble/hashes": "npm:^1.8.0" + "@scure/bip32": "npm:^1.7.0" + "@scure/bip39": "npm:^1.6.0" + abitype: "npm:^1.2.3" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/96526073193f3a6dd2ccd21bcc255e82c7226d6de63fa17a2021c75232fdc9bc969e75e2cbc0c8d5163d88c575e08dc4c75dec7333b1727f080585f07fc6c1ed + languageName: node + linkType: hard + +"ox@npm:0.8.1": + version: 0.8.1 + resolution: "ox@npm:0.8.1" + dependencies: + "@adraffy/ens-normalize": "npm:^1.11.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/curves": "npm:^1.9.1" + "@noble/hashes": "npm:^1.8.0" + "@scure/bip32": "npm:^1.7.0" + "@scure/bip39": "npm:^1.6.0" + abitype: "npm:^1.0.8" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/a3c967e5b30792d89e7ecbdf976c00c625738e96263e1f0a95ad43c27b57ac18f21357eb7a651ce3c0ff0dc54b3ed071516c9804bc48fa2134262a5066b62fcc + languageName: node + linkType: hard + "oxfmt@npm:^0.44.0": version: 0.44.0 resolution: "oxfmt@npm:0.44.0" @@ -15052,6 +15159,27 @@ __metadata: languageName: node linkType: hard +"viem@npm:^2.46.2": + version: 2.48.4 + resolution: "viem@npm:2.48.4" + dependencies: + "@noble/curves": "npm:1.9.1" + "@noble/hashes": "npm:1.8.0" + "@scure/bip32": "npm:1.7.0" + "@scure/bip39": "npm:1.6.0" + abitype: "npm:1.2.3" + isows: "npm:1.0.7" + ox: "npm:0.14.20" + ws: "npm:8.18.3" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/79ab1c8941013e1b4d12ef0bd7fcca6108cfc078b669cc02ae5a08c94d4e3b6de182071cfb40fb4e33ddc40b3aa997f3ebb50d269c85512cefcefdce49b193a0 + languageName: node + linkType: hard + "vscode-oniguruma@npm:^1.7.0": version: 1.7.0 resolution: "vscode-oniguruma@npm:1.7.0" From 1b0cc82c337f2f1d47ed1ecf63d260750337f0ff Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 30 Apr 2026 12:22:17 +0200 Subject: [PATCH 2/3] chore: dedupe yarn.lock --- yarn.lock | 48 +++--------------------------------------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/yarn.lock b/yarn.lock index 82fb89ad071..a058e070ace 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7442,7 +7442,7 @@ __metadata: languageName: node linkType: hard -"abitype@npm:1.2.3, abitype@npm:^1.2.3": +"abitype@npm:1.2.3": version: 1.2.3 resolution: "abitype@npm:1.2.3" peerDependencies: @@ -7457,7 +7457,7 @@ __metadata: languageName: node linkType: hard -"abitype@npm:^1.0.8": +"abitype@npm:^1.0.8, abitype@npm:^1.2.3": version: 1.2.4 resolution: "abitype@npm:1.2.4" peerDependencies: @@ -12929,27 +12929,6 @@ __metadata: languageName: node linkType: hard -"ox@npm:0.12.4": - version: 0.12.4 - resolution: "ox@npm:0.12.4" - dependencies: - "@adraffy/ens-normalize": "npm:^1.11.0" - "@noble/ciphers": "npm:^1.3.0" - "@noble/curves": "npm:1.9.1" - "@noble/hashes": "npm:^1.8.0" - "@scure/bip32": "npm:^1.7.0" - "@scure/bip39": "npm:^1.6.0" - abitype: "npm:^1.2.3" - eventemitter3: "npm:5.0.1" - peerDependencies: - typescript: ">=5.4.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/077509b841658693a411df505d0bdbbee2d68734aa19736ccff5a6087c119c4aebc1d8d8c2039ca9f16ae7430cb44812e4c182f858cab67c9a755dd0e9914178 - languageName: node - linkType: hard - "ox@npm:0.14.20": version: 0.14.20 resolution: "ox@npm:0.14.20" @@ -15138,28 +15117,7 @@ __metadata: languageName: node linkType: hard -"viem@npm:^2.36.0": - version: 2.46.2 - resolution: "viem@npm:2.46.2" - dependencies: - "@noble/curves": "npm:1.9.1" - "@noble/hashes": "npm:1.8.0" - "@scure/bip32": "npm:1.7.0" - "@scure/bip39": "npm:1.6.0" - abitype: "npm:1.2.3" - isows: "npm:1.0.7" - ox: "npm:0.12.4" - ws: "npm:8.18.3" - peerDependencies: - typescript: ">=5.0.4" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/dd763503c9fc7c3c2908f8cd403f375a0c313d0ded7aeeef87e1672553fc75cca070ed02e2d811ccc5d3cfb7a589be23e45cb147a556a0a0751adbb3f77be265 - languageName: node - linkType: hard - -"viem@npm:^2.46.2": +"viem@npm:^2.36.0, viem@npm:^2.46.2": version: 2.48.4 resolution: "viem@npm:2.48.4" dependencies: From ca349826586632657210c55d63981815a143c97f Mon Sep 17 00:00:00 2001 From: John Whiles Date: Thu, 30 Apr 2026 12:54:22 +0200 Subject: [PATCH 3/3] feat: improve build delegations step implementation --- .../src/MoneyAccountUpgradeController.test.ts | 3 +- .../src/MoneyAccountUpgradeController.ts | 10 +-- .../src/steps/associate-address.test.ts | 63 +++++++----------- .../src/steps/build-delegations.test.ts | 66 ++++++++++++------- .../src/steps/build-delegations.ts | 63 +++++++----------- .../src/steps/eip-7702-authorization.test.ts | 6 ++ .../src/steps/step.ts | 5 +- .../src/types.ts | 6 +- 8 files changed, 107 insertions(+), 115 deletions(-) diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts index a2407a78845..ffb189d4105 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.test.ts @@ -27,7 +27,6 @@ const MOCK_CONFIG: UpgradeConfig = { const MOCK_INIT_CONFIG = { delegatorImplAddress: MOCK_CONFIG.delegatorImplAddress, - musdTokenAddress: MOCK_CONFIG.musdTokenAddress, redeemerEnforcer: MOCK_CONFIG.redeemerEnforcer, valueLteEnforcer: MOCK_CONFIG.valueLteEnforcer, }; @@ -41,7 +40,7 @@ const MOCK_SERVICE_DETAILS_RESPONSE = { vedaProtocol: { supportedTokens: [ { - tokenAddress: MOCK_CONFIG.erc20TransferAmountEnforcer, + tokenAddress: MOCK_CONFIG.musdTokenAddress, tokenDecimals: 18, }, ], diff --git a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts index a62abc12e33..27e3363fe1a 100644 --- a/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts +++ b/packages/money-account-upgrade-controller/src/MoneyAccountUpgradeController.ts @@ -88,9 +88,10 @@ export class MoneyAccountUpgradeController extends BaseController< > { #config?: { chainId: Hex; + delegateAddress: Hex; delegatorImplAddress: Hex; - tokenAddress: Hex; - redeemerAddress: Hex; + musdTokenAddress: Hex; + vedaVaultAdapterAddress: Hex; }; readonly #steps: Step[] = [ @@ -156,9 +157,10 @@ export class MoneyAccountUpgradeController extends BaseController< this.#config = { chainId, + delegateAddress: chain.autoDepositDelegate, delegatorImplAddress: initConfig.delegatorImplAddress, - tokenAddress: initConfig.musdTokenAddress, - redeemerAddress: initConfig.vedaVaultAdapterAddress, + musdTokenAddress: vedaProtocol.supportedTokens[0].tokenAddress, + vedaVaultAdapterAddress: vedaProtocol.adapterAddress, }; } diff --git a/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts b/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts index b141dc0ef45..9aa7513e38e 100644 --- a/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts +++ b/packages/money-account-upgrade-controller/src/steps/associate-address.test.ts @@ -11,7 +11,10 @@ import { associateAddressStep } from './associate-address'; const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; const MOCK_CHAIN_ID = '0x1' as Hex; +const MOCK_DELEGATE = '0x1111111111111111111111111111111111111111' as Hex; const MOCK_DELEGATOR_IMPL = '0x2222222222222222222222222222222222222222' as Hex; +const MOCK_TOKEN = '0x3333333333333333333333333333333333333333' as Hex; +const MOCK_VAULT_ADAPTER = '0x4444444444444444444444444444444444444444' as Hex; const MOCK_SIGNATURE = '0xdeadbeefcafebabe'; const MOCK_NOW = new Date('2026-04-17T12:00:00.000Z').getTime(); @@ -64,6 +67,20 @@ function setup(): { return { messenger, mocks }; } +async function run( + messenger: MoneyAccountUpgradeControllerMessenger, +): ReturnType { + return associateAddressStep.run({ + messenger, + address: MOCK_ADDRESS, + chainId: MOCK_CHAIN_ID, + delegateAddress: MOCK_DELEGATE, + delegatorImplAddress: MOCK_DELEGATOR_IMPL, + musdTokenAddress: MOCK_TOKEN, + vedaVaultAdapterAddress: MOCK_VAULT_ADAPTER, + }); +} + describe('associateAddressStep', () => { beforeEach(() => { jest.useFakeTimers(); @@ -81,12 +98,7 @@ describe('associateAddressStep', () => { it('signs the CHOMP Authentication message with the given address', async () => { const { messenger, mocks } = setup(); - await associateAddressStep.run({ - messenger, - address: MOCK_ADDRESS, - chainId: MOCK_CHAIN_ID, - delegatorImplAddress: MOCK_DELEGATOR_IMPL, - }); + await run(messenger); expect(mocks.signPersonalMessage).toHaveBeenCalledWith({ data: `CHOMP Authentication ${MOCK_NOW}`, @@ -97,12 +109,7 @@ describe('associateAddressStep', () => { it('submits the signature, timestamp, and address to the CHOMP API', async () => { const { messenger, mocks } = setup(); - await associateAddressStep.run({ - messenger, - address: MOCK_ADDRESS, - chainId: MOCK_CHAIN_ID, - delegatorImplAddress: MOCK_DELEGATOR_IMPL, - }); + await run(messenger); expect(mocks.associateAddress).toHaveBeenCalledWith({ signature: MOCK_SIGNATURE, @@ -114,12 +121,7 @@ describe('associateAddressStep', () => { it('returns "completed" when CHOMP creates the association', async () => { const { messenger } = setup(); - const result = await associateAddressStep.run({ - messenger, - address: MOCK_ADDRESS, - chainId: MOCK_CHAIN_ID, - delegatorImplAddress: MOCK_DELEGATOR_IMPL, - }); + const result = await run(messenger); expect(result).toBe('completed'); }); @@ -131,12 +133,7 @@ describe('associateAddressStep', () => { status: 'active', }); - const result = await associateAddressStep.run({ - messenger, - address: MOCK_ADDRESS, - chainId: MOCK_CHAIN_ID, - delegatorImplAddress: MOCK_DELEGATOR_IMPL, - }); + const result = await run(messenger); expect(result).toBe('already-done'); }); @@ -145,14 +142,7 @@ describe('associateAddressStep', () => { const { messenger, mocks } = setup(); mocks.signPersonalMessage.mockRejectedValue(new Error('signing failed')); - await expect( - associateAddressStep.run({ - messenger, - address: MOCK_ADDRESS, - chainId: MOCK_CHAIN_ID, - delegatorImplAddress: MOCK_DELEGATOR_IMPL, - }), - ).rejects.toThrow('signing failed'); + await expect(run(messenger)).rejects.toThrow('signing failed'); expect(mocks.associateAddress).not.toHaveBeenCalled(); }); @@ -160,13 +150,6 @@ describe('associateAddressStep', () => { const { messenger, mocks } = setup(); mocks.associateAddress.mockRejectedValue(new Error('api failed')); - await expect( - associateAddressStep.run({ - messenger, - address: MOCK_ADDRESS, - chainId: MOCK_CHAIN_ID, - delegatorImplAddress: MOCK_DELEGATOR_IMPL, - }), - ).rejects.toThrow('api failed'); + await expect(run(messenger)).rejects.toThrow('api failed'); }); }); diff --git a/packages/money-account-upgrade-controller/src/steps/build-delegations.test.ts b/packages/money-account-upgrade-controller/src/steps/build-delegations.test.ts index ad8280b16c3..12863ac7552 100644 --- a/packages/money-account-upgrade-controller/src/steps/build-delegations.test.ts +++ b/packages/money-account-upgrade-controller/src/steps/build-delegations.test.ts @@ -11,15 +11,13 @@ import { } from '@metamask/smart-accounts-kit'; import { hashDelegation, + SIGNABLE_DELEGATION_TYPED_DATA, toDelegationStruct, } from '@metamask/smart-accounts-kit/utils'; import type { Hex } from '@metamask/utils'; import type { MoneyAccountUpgradeControllerMessenger } from '../MoneyAccountUpgradeController'; -import { - buildDelegationsSteps, - SIGNABLE_DELEGATION_TYPED_DATA, -} from './build-delegations'; +import { buildDelegationStep } from './build-delegations'; jest.mock('@metamask/smart-accounts-kit', () => ({ createDelegation: jest.fn(), @@ -30,6 +28,7 @@ jest.mock( '@metamask/smart-accounts-kit/utils', () => ({ hashDelegation: jest.fn(), + SIGNABLE_DELEGATION_TYPED_DATA: { Delegation: [] }, toDelegationStruct: jest.fn(), }), { virtual: true }, @@ -43,25 +42,24 @@ const mockToDelegationStruct = jest.mocked(toDelegationStruct); const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; const MOCK_CHAIN_ID = '0xaa36a7' as Hex; // 11155111 (Sepolia) const MOCK_CHAIN_ID_DECIMAL = 11155111; +const MOCK_DELEGATE = '0x1111111111111111111111111111111111111111' as Hex; const MOCK_DELEGATOR_IMPL = '0x2222222222222222222222222222222222222222' as Hex; const MOCK_TOKEN = '0x3333333333333333333333333333333333333333' as Hex; -const MOCK_REDEEMER = '0x4444444444444444444444444444444444444444' as Hex; +const MOCK_VAULT_ADAPTER = '0x4444444444444444444444444444444444444444' as Hex; const MOCK_DELEGATION_MANAGER = '0x5555555555555555555555555555555555555555' as Hex; const MOCK_HASH: Hex = `0x${'ab'.repeat(32)}`; const MOCK_SIGNATURE: Hex = `0x${'cd'.repeat(65)}`; const NON_MATCHING_HASH: Hex = `0x${'ef'.repeat(32)}`; -const AAVE_ADAPTER_ADDRESS = '0xbab56C2Ea37C5976247eF2dAfBf7FC4B97e7Af1c'; -const MAX_UINT256 = - '115792089237316195423570985008687907853269984665640564039457584007913129639935'; +const MAX_UINT256 = 2n ** 256n - 1n; const MOCK_ENVIRONMENT = { DelegationManager: MOCK_DELEGATION_MANAGER, } as ReturnType; const MOCK_DELEGATION: ReturnType = { - delegate: MOCK_REDEEMER, + delegate: MOCK_DELEGATE, delegator: MOCK_ADDRESS, authority: '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' as Hex, @@ -97,7 +95,9 @@ function setup(): { const mocks: Mocks = { listDelegations: jest.fn().mockResolvedValue([]), signTypedMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE), - verifyDelegation: jest.fn().mockResolvedValue({ valid: true }), + verifyDelegation: jest + .fn() + .mockResolvedValue({ valid: true, delegationHash: MOCK_HASH }), }; const rootMessenger = new Messenger({ @@ -135,18 +135,19 @@ function setup(): { async function run( messenger: MoneyAccountUpgradeControllerMessenger, -): ReturnType { - return buildDelegationsSteps.run({ +): ReturnType { + return buildDelegationStep.run({ messenger, address: MOCK_ADDRESS, chainId: MOCK_CHAIN_ID, + delegateAddress: MOCK_DELEGATE, delegatorImplAddress: MOCK_DELEGATOR_IMPL, - tokenAddress: MOCK_TOKEN, - redeemerAddress: MOCK_REDEEMER, + musdTokenAddress: MOCK_TOKEN, + vedaVaultAdapterAddress: MOCK_VAULT_ADAPTER, }); } -describe('buildDelegationsSteps', () => { +describe('buildDelegationStep', () => { beforeEach(() => { mockGetEnvironment.mockReturnValue(MOCK_ENVIRONMENT); mockCreateDelegation.mockReturnValue(MOCK_DELEGATION); @@ -158,8 +159,8 @@ describe('buildDelegationsSteps', () => { jest.clearAllMocks(); }); - it('is named "build-delegations"', () => { - expect(buildDelegationsSteps.name).toBe('build-delegations'); + it('is named "build-delegation"', () => { + expect(buildDelegationStep.name).toBe('build-delegation'); }); it('builds the delegation against the chain-specific environment using a fresh 32-byte salt', async () => { @@ -173,15 +174,13 @@ describe('buildDelegationsSteps', () => { scope: { type: 'erc20TransferAmount', tokenAddress: MOCK_TOKEN, - maxAmount: BigInt(MAX_UINT256), + maxAmount: MAX_UINT256, }, from: MOCK_ADDRESS, - to: MOCK_REDEEMER, + to: MOCK_DELEGATE, caveats: [ - { - type: 'redeemer', - redeemers: [AAVE_ADAPTER_ADDRESS], - }, + { type: 'redeemer', redeemers: [MOCK_VAULT_ADAPTER] }, + { type: 'valueLte', maxValue: 0n }, ], // 32-byte 0x-prefixed hex string. salt: expect.stringMatching(/^0x[0-9a-f]{64}$/u) as Hex, @@ -282,6 +281,27 @@ describe('buildDelegationsSteps', () => { expect(result).toBe('completed'); }); + + it('throws when CHOMP rejects the delegation', async () => { + const { messenger, mocks } = setup(); + mocks.verifyDelegation.mockResolvedValue({ + valid: false, + errors: ['caveat mismatch', 'unknown enforcer'], + }); + + await expect(run(messenger)).rejects.toThrow( + 'CHOMP rejected delegation: caveat mismatch, unknown enforcer', + ); + }); + + it('throws with a default message when CHOMP rejects without errors', async () => { + const { messenger, mocks } = setup(); + mocks.verifyDelegation.mockResolvedValue({ valid: false }); + + await expect(run(messenger)).rejects.toThrow( + 'CHOMP rejected delegation: unknown error', + ); + }); }); describe('error propagation', () => { diff --git a/packages/money-account-upgrade-controller/src/steps/build-delegations.ts b/packages/money-account-upgrade-controller/src/steps/build-delegations.ts index 6a3ae1beb09..6c8e008356c 100644 --- a/packages/money-account-upgrade-controller/src/steps/build-delegations.ts +++ b/packages/money-account-upgrade-controller/src/steps/build-delegations.ts @@ -6,64 +6,43 @@ import { } from '@metamask/smart-accounts-kit'; import { hashDelegation, + SIGNABLE_DELEGATION_TYPED_DATA, toDelegationStruct, } from '@metamask/smart-accounts-kit/utils'; import { hexToNumber, bytesToHex } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; -import { webcrypto } from 'node:crypto'; import type { Step } from './step'; -const MAX_UINT256 = - '115792089237316195423570985008687907853269984665640564039457584007913129639935'; - -// Sourced from https://github.com/MetaMask/snap-cash-account-poc/blob/70709e15ddc56288dd9eefa45b425a756f25d2fb/packages/snap/src/api/config.ts#L39-L40 -const AAVE_ADAPTER_ADDRESS = '0xbab56C2Ea37C5976247eF2dAfBf7FC4B97e7Af1c'; - -/** - * EIP-712 typed data structure for signing delegations. - */ -export const SIGNABLE_DELEGATION_TYPED_DATA = { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - Caveat: [ - { name: 'enforcer', type: 'address' }, - { name: 'terms', type: 'bytes' }, - ], - Delegation: [ - { name: 'delegate', type: 'address' }, - { name: 'delegator', type: 'address' }, - { name: 'authority', type: 'bytes32' }, - { name: 'caveats', type: 'Caveat[]' }, - { name: 'salt', type: 'uint256' }, - ], -} as const; +const MAX_UINT256 = 2n ** 256n - 1n; export const buildDelegationStep: Step = { name: 'build-delegation', - async run({ messenger, address, redeemerAddress, chainId, tokenAddress }) { - const saltBytes = webcrypto.getRandomValues(new Uint8Array(32)); + async run({ + messenger, + address, + chainId, + delegateAddress, + musdTokenAddress, + vedaVaultAdapterAddress, + }) { + const saltBytes = globalThis.crypto.getRandomValues(new Uint8Array(32)); const salt = bytesToHex(saltBytes); const chainIdDecimal = hexToNumber(chainId); const environment = getSmartAccountsEnvironment(chainIdDecimal); + const delegation = createDelegation({ environment, scope: { type: 'erc20TransferAmount', - tokenAddress, - maxAmount: BigInt(MAX_UINT256), + tokenAddress: musdTokenAddress, + maxAmount: MAX_UINT256, }, from: address, - to: redeemerAddress, + to: delegateAddress, caveats: [ - { - type: 'redeemer', - redeemers: [AAVE_ADAPTER_ADDRESS], - }, + { type: 'redeemer', redeemers: [vedaVaultAdapterAddress] }, + { type: 'valueLte', maxValue: 0n }, ], salt, }); @@ -100,11 +79,17 @@ export const buildDelegationStep: Step = { SignTypedDataVersion.V4, )) as Hex; - await messenger.call('ChompApiService:verifyDelegation', { + const result = await messenger.call('ChompApiService:verifyDelegation', { signedDelegation: { ...delegation, signature }, chainId, }); + if (!result.valid) { + throw new Error( + `CHOMP rejected delegation: ${result.errors?.join(', ') ?? 'unknown error'}`, + ); + } + return 'completed'; }, }; diff --git a/packages/money-account-upgrade-controller/src/steps/eip-7702-authorization.test.ts b/packages/money-account-upgrade-controller/src/steps/eip-7702-authorization.test.ts index 64a562ed0a5..6e57632938b 100644 --- a/packages/money-account-upgrade-controller/src/steps/eip-7702-authorization.test.ts +++ b/packages/money-account-upgrade-controller/src/steps/eip-7702-authorization.test.ts @@ -12,7 +12,10 @@ import { eip7702AuthorizationStep } from './eip-7702-authorization'; const MOCK_ADDRESS = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; const MOCK_CHAIN_ID = '0xaa36a7' as Hex; // 11155111 (Sepolia) — non-trivial decimal const MOCK_CHAIN_ID_DECIMAL = parseInt(MOCK_CHAIN_ID, 16); +const MOCK_DELEGATE = '0x1111111111111111111111111111111111111111' as Hex; const MOCK_DELEGATOR_IMPL = '0x2222222222222222222222222222222222222222' as Hex; +const MOCK_TOKEN = '0x3333333333333333333333333333333333333333' as Hex; +const MOCK_VAULT_ADAPTER = '0x4444444444444444444444444444444444444444' as Hex; const MOCK_THIRD_PARTY_IMPL = '0x9999999999999999999999999999999999999999' as Hex; const MOCK_NETWORK_CLIENT_ID = 'network-client-id'; @@ -142,7 +145,10 @@ async function run( messenger, address: MOCK_ADDRESS, chainId: MOCK_CHAIN_ID, + delegateAddress: MOCK_DELEGATE, delegatorImplAddress: MOCK_DELEGATOR_IMPL, + musdTokenAddress: MOCK_TOKEN, + vedaVaultAdapterAddress: MOCK_VAULT_ADAPTER, }); } diff --git a/packages/money-account-upgrade-controller/src/steps/step.ts b/packages/money-account-upgrade-controller/src/steps/step.ts index 845d704ec51..acda2f6b4b7 100644 --- a/packages/money-account-upgrade-controller/src/steps/step.ts +++ b/packages/money-account-upgrade-controller/src/steps/step.ts @@ -9,9 +9,10 @@ export type StepContext = { messenger: MoneyAccountUpgradeControllerMessenger; address: Hex; chainId: Hex; + delegateAddress: Hex; delegatorImplAddress: Hex; - tokenAddress: Hex; - redeemerAddress: Hex; + musdTokenAddress: Hex; + vedaVaultAdapterAddress: Hex; }; /** diff --git a/packages/money-account-upgrade-controller/src/types.ts b/packages/money-account-upgrade-controller/src/types.ts index abc3661f164..8357866ca01 100644 --- a/packages/money-account-upgrade-controller/src/types.ts +++ b/packages/money-account-upgrade-controller/src/types.ts @@ -27,9 +27,5 @@ export type UpgradeConfig = { */ export type InitConfig = Pick< UpgradeConfig, - | 'delegatorImplAddress' - | 'musdTokenAddress' - | 'redeemerEnforcer' - | 'valueLteEnforcer' - | 'vedaVaultAdapterAddress' + 'delegatorImplAddress' | 'redeemerEnforcer' | 'valueLteEnforcer' >;