Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ linkStyle default opacity:0.5
money_account_controller --> base_controller;
money_account_controller --> keyring_controller;
money_account_controller --> messenger;
money_account_upgrade_controller --> authenticated_user_storage;
money_account_upgrade_controller --> base_controller;
money_account_upgrade_controller --> chomp_api_service;
money_account_upgrade_controller --> keyring_controller;
Expand Down
11 changes: 11 additions & 0 deletions packages/money-account-upgrade-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `build-delegation` step to the upgrade sequence ([#8621](https://github.com/MetaMask/core/pull/8621))

### Changed

- **BREAKING:** The controller messenger now requires access to three additional allowed actions: `ChompApiService:verifyDelegation`, `KeyringController:signTypedMessage`, and `AuthenticatedUserStorageService:listDelegations`. Consumers must update their messenger configuration accordingly. ([#8621](https://github.com/MetaMask/core/pull/8621))
- **BREAKING:** `InitConfig` no longer includes `musdTokenAddress`; it is now derived internally from the Veda protocol service details. ([#8621](https://github.com/MetaMask/core/pull/8621))
- **BREAKING:** `InitConfig` now also requires `erc20TransferAmountEnforcer`; the build-delegation step uses it (along with `redeemerEnforcer` and `valueLteEnforcer`) to pin caveat enforcer deployments rather than relying on `@metamask/smart-accounts-kit`'s registry. ([#8621](https://github.com/MetaMask/core/pull/8621))
- Add `@metamask/authenticated-user-storage`, `@metamask/smart-accounts-kit`, `uuid`, and `viem` as dependencies. ([#8621](https://github.com/MetaMask/core/pull/8621))

## [1.3.1]

### Changed
Expand Down
25 changes: 19 additions & 6 deletions packages/money-account-upgrade-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
* https://jestjs.io/docs/configuration
*/

const merge = require('deepmerge');
const path = require('path');

const baseConfig = require('../../jest.config.packages');

// Transitive dep of `@metamask/smart-accounts-kit`; resolved up-front so the
// base config's `^@metamask/(.+)$` mapper doesn't rewrite it to a missing path.
const delegationAbisBytecodePath =
// eslint-disable-next-line n/no-extraneous-require
require.resolve('@metamask/delegation-abis/bytecode');

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,
Expand All @@ -23,4 +26,14 @@ 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'),
'^@metamask/delegation-abis/bytecode$': delegationAbisBytecodePath,
...baseConfig.moduleNameMapper,
},
};
6 changes: 5 additions & 1 deletion packages/money-account-upgrade-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.4.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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const MOCK_CONFIG: UpgradeConfig = {

const MOCK_INIT_CONFIG = {
delegatorImplAddress: MOCK_CONFIG.delegatorImplAddress,
musdTokenAddress: MOCK_CONFIG.musdTokenAddress,
erc20TransferAmountEnforcer: MOCK_CONFIG.erc20TransferAmountEnforcer,
redeemerEnforcer: MOCK_CONFIG.redeemerEnforcer,
valueLteEnforcer: MOCK_CONFIG.valueLteEnforcer,
};
Expand All @@ -41,7 +41,7 @@ const MOCK_SERVICE_DETAILS_RESPONSE = {
vedaProtocol: {
supportedTokens: [
{
tokenAddress: MOCK_CONFIG.erc20TransferAmountEnforcer,
tokenAddress: MOCK_CONFIG.musdTokenAddress,
tokenDecimals: 18,
},
],
Expand All @@ -68,6 +68,9 @@ type Mocks = {
findNetworkClientIdByChainId: jest.Mock;
getNetworkClientById: jest.Mock;
providerRequest: jest.Mock;
listDelegations: jest.Mock;
signTypedMessage: jest.Mock;
verifyDelegation: jest.Mock;
};

function setup(): {
Expand Down Expand Up @@ -118,6 +121,9 @@ function setup(): {
provider: { request: providerRequest },
}),
providerRequest,
listDelegations: jest.fn().mockResolvedValue([]),
signTypedMessage: jest.fn().mockResolvedValue(`0x${'cd'.repeat(65)}`),
verifyDelegation: jest.fn().mockResolvedValue({ valid: true }),
};

const rootMessenger = new Messenger<MockAnyNamespace, AllActions, AllEvents>({
Expand Down Expand Up @@ -152,6 +158,18 @@ function setup(): {
'NetworkController:getNetworkClientById',
mocks.getNetworkClientById,
);
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',
Expand All @@ -167,6 +185,9 @@ function setup(): {
'KeyringController:signEip7702Authorization',
'NetworkController:findNetworkClientIdByChainId',
'NetworkController:getNetworkClientById',
'AuthenticatedUserStorageService:listDelegations',
'KeyringController:signTypedMessage',
'ChompApiService:verifyDelegation',
],
events: [],
messenger,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AuthenticatedUserStorageServiceListDelegationsAction } from '@metamask/authenticated-user-storage';
import type {
ControllerGetStateAction,
ControllerStateChangedEvent,
Expand All @@ -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 {
Expand All @@ -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';
Expand Down Expand Up @@ -49,10 +53,13 @@ type AllowedActions =
| ChompApiServiceAssociateAddressAction
| ChompApiServiceCreateUpgradeAction
| ChompApiServiceGetServiceDetailsAction
| ChompApiServiceVerifyDelegationAction
| KeyringControllerSignEip7702AuthorizationAction
| KeyringControllerSignPersonalMessageAction
| KeyringControllerSignTypedMessageAction
| NetworkControllerFindNetworkClientIdByChainIdAction
| NetworkControllerGetNetworkClientByIdAction;
| NetworkControllerGetNetworkClientByIdAction
| AuthenticatedUserStorageServiceListDelegationsAction;

export type MoneyAccountUpgradeControllerStateChangedEvent =
ControllerStateChangedEvent<
Expand All @@ -79,9 +86,22 @@ export class MoneyAccountUpgradeController extends BaseController<
MoneyAccountUpgradeControllerState,
MoneyAccountUpgradeControllerMessenger
> {
#config?: { chainId: Hex; delegatorImplAddress: Hex };

readonly #steps: Step[] = [associateAddressStep, eip7702AuthorizationStep];
#config?: {
chainId: Hex;
delegateAddress: Hex;
delegatorImplAddress: Hex;
erc20TransferAmountEnforcer: Hex;
musdTokenAddress: Hex;
redeemerEnforcer: Hex;
valueLteEnforcer: Hex;
vedaVaultAdapterAddress: Hex;
};

readonly #steps: Step[] = [
associateAddressStep,
eip7702AuthorizationStep,
buildDelegationStep,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we actually have to build 2 delegations. Both are pretty much the same except the token address is different. One is for deposits (mUSD) and second is for withdrawals (vmUSD => the boringVaultAddress)

];

/**
* Constructor for the MoneyAccountUpgradeController.
Expand Down Expand Up @@ -140,7 +160,13 @@ export class MoneyAccountUpgradeController extends BaseController<

this.#config = {
chainId,
delegateAddress: chain.autoDepositDelegate,
delegatorImplAddress: initConfig.delegatorImplAddress,
erc20TransferAmountEnforcer: initConfig.erc20TransferAmountEnforcer,
musdTokenAddress: vedaProtocol.supportedTokens[0].tokenAddress,
redeemerEnforcer: initConfig.redeemerEnforcer,
valueLteEnforcer: initConfig.valueLteEnforcer,
vedaVaultAdapterAddress: vedaProtocol.adapterAddress,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ 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_ERC20_ENFORCER = '0x5555555555555555555555555555555555555555' as Hex;
const MOCK_REDEEMER_ENFORCER =
'0x6666666666666666666666666666666666666666' as Hex;
const MOCK_VALUE_LTE_ENFORCER =
'0x7777777777777777777777777777777777777777' as Hex;
const MOCK_SIGNATURE = '0xdeadbeefcafebabe';
const MOCK_NOW = new Date('2026-04-17T12:00:00.000Z').getTime();

Expand Down Expand Up @@ -64,6 +72,23 @@ function setup(): {
return { messenger, mocks };
}

async function run(
messenger: MoneyAccountUpgradeControllerMessenger,
): ReturnType<typeof associateAddressStep.run> {
return associateAddressStep.run({
messenger,
address: MOCK_ADDRESS,
chainId: MOCK_CHAIN_ID,
delegateAddress: MOCK_DELEGATE,
delegatorImplAddress: MOCK_DELEGATOR_IMPL,
erc20TransferAmountEnforcer: MOCK_ERC20_ENFORCER,
musdTokenAddress: MOCK_TOKEN,
redeemerEnforcer: MOCK_REDEEMER_ENFORCER,
valueLteEnforcer: MOCK_VALUE_LTE_ENFORCER,
vedaVaultAdapterAddress: MOCK_VAULT_ADAPTER,
});
}

describe('associateAddressStep', () => {
beforeEach(() => {
jest.useFakeTimers();
Expand All @@ -81,12 +106,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}`,
Expand All @@ -97,12 +117,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,
Expand All @@ -114,12 +129,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');
});
Expand All @@ -131,12 +141,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');
});
Expand All @@ -145,28 +150,14 @@ 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();
});

it('propagates errors from the CHOMP API', async () => {
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');
});
});
Loading
Loading