Skip to content
Closed
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
5 changes: 1 addition & 4 deletions packages/delegation-toolkit/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
/* eslint-disable */
// experimental actions will be moved here once they have stabilized

// signDelegation action will be added here
export { isEip7702StatelessDelegatedAccount } from './isEip7702StatelessDelegatedAccount';
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { EIP7702StatelessDeleGator } from '@metamask/delegation-abis';
import type { Client, Address } from 'viem';
import { readContract } from 'viem/actions';

/**
* Parameters for checking if an EOA is delegated to the EIP7702StatelessDeleGator.
*/
export type IsEip7702StatelessDelegatedAccountParameters = {
/** The client to use for the query. */
client: Client;
/** The address to check for EIP7702 delegation to StatelessDeleGator. */
accountAddress: Address;
};

/**
* Checks if an EOA is specifically delegated to the EIP7702StatelessDeleGator implementation.
*
* This function goes beyond simply checking if code exists at the address (which could be any contract
* or any EIP7702 delegation). It specifically verifies that the account is delegated to the
* EIP7702StatelessDeleGator by calling the NAME() function and ensuring it returns "EIP7702StatelessDeleGator".
*
* @param params - The parameters for checking the delegation.
* @param params.client - The client to use for the query.
* @param params.accountAddress - The address to check for EIP7702 delegation.
* @returns A promise that resolves to true if the account is delegated to EIP7702StatelessDeleGator, false otherwise.
* @example
* ```typescript
* const isDelegated = await isEip7702StatelessDelegatedAccount({
* client: publicClient,
* accountAddress: '0x...',
* });
*
* if (isDelegated) {
* console.log('Account is delegated to EIP7702StatelessDeleGator');
* } else {
* console.log('Account is not delegated to EIP7702StatelessDeleGator');
* }
* ```
*/
export async function isEip7702StatelessDelegatedAccount({
client,
accountAddress,
}: IsEip7702StatelessDelegatedAccountParameters): Promise<boolean> {
try {
const contractName = await readContract(client, {
address: accountAddress,
abi: EIP7702StatelessDeleGator.abi,
functionName: 'NAME',
});

return contractName === 'EIP7702StatelessDeleGator';
} catch (error) {
// If the call fails (e.g., no code at address, or NAME() function doesn't exist),
// then it's not delegated to our implementation
return false;
}
}
2 changes: 2 additions & 0 deletions packages/delegation-toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ export type { AggregateSignatureParams } from './signatures';
export { signUserOperation } from './userOp';

export { redeemDelegations } from './write';

export { isEip7702StatelessDelegatedAccount } from './actions';
13 changes: 13 additions & 0 deletions packages/delegation-toolkit/src/toMetaMaskSmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
toSmartAccount,
} from 'viem/account-abstraction';

import { isEip7702StatelessDelegatedAccount } from './actions/isEip7702StatelessDelegatedAccount';
import { Implementation } from './constants';
import { getCounterfactualAccountData } from './counterfactualAccountData';
import {
Expand Down Expand Up @@ -197,5 +198,17 @@ export async function toMetaMaskSmartAccount<
...signatory,
});

// For Stateless7702, override isDeployed to check specific delegation
if (implementation === Implementation.Stateless7702) {
return {
...smartAccount,
isDeployed: async () =>
isEip7702StatelessDelegatedAccount({
client,
accountAddress: address,
}),
};
}

return smartAccount;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { PublicClient } from 'viem';
import { createPublicClient, http } from 'viem';
import { foundry } from 'viem/chains';
import { beforeEach, describe, expect, it } from 'vitest';

import { isEip7702StatelessDelegatedAccount } from '../../src/actions/isEip7702StatelessDelegatedAccount';

describe('isEip7702StatelessDelegatedAccount', () => {
let publicClient: PublicClient;

beforeEach(() => {
publicClient = createPublicClient({
chain: foundry,
transport: http(),
});
});

it('should return false for an address with no code', async () => {
const randomAddress = '0x1234567890123456789012345678901234567890';

const result = await isEip7702StatelessDelegatedAccount({
client: publicClient,
accountAddress: randomAddress,
});

expect(result).toBe(false);
});

it('should return false for an address that does not implement NAME() function', async () => {
// Use an address that we know has code but doesn't implement NAME()
// In this case, we'll use the zero address which should fail
const zeroAddress = '0x0000000000000000000000000000000000000000';

const result = await isEip7702StatelessDelegatedAccount({
client: publicClient,
accountAddress: zeroAddress,
});

expect(result).toBe(false);
});

it('should return false for a contract that implements NAME() but returns a different name', async () => {
// This test would need a mock contract that implements NAME() but returns something else
// For now, we'll just test the error handling path with an invalid address
const invalidAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd';

const result = await isEip7702StatelessDelegatedAccount({
client: publicClient,
accountAddress: invalidAddress,
});

expect(result).toBe(false);
});

// Note: A full integration test would require:
// 1. Deploying the EIP7702StatelessDeleGator contract
// 2. Creating an EOA and delegating it to that contract using EIP-7702
// 3. Testing that the function returns true for that delegated account
// This would be better suited for the e2e test suite
});
114 changes: 114 additions & 0 deletions packages/delegator-e2e/test/execute-stateless7702.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,117 @@ test('Alice can check the contract version and name', async () => {

expect(domainVersion, 'Domain version should be 1').toBe('1');
});

test('isEip7702StatelessDelegatedAccount correctly identifies delegated accounts', async () => {
const { isEip7702StatelessDelegatedAccount } = await import(
'@metamask/delegation-toolkit'
);

// Test that Alice's account (which is delegated to EIP7702StatelessDeleGator) returns true
const isDelegated = await isEip7702StatelessDelegatedAccount({
client: publicClient,
accountAddress: aliceSmartAccount.address,
});

expect(
isDelegated,
'Alice account should be delegated to EIP7702StatelessDeleGator',
).toBe(true);

// Test with a random non-delegated address
const randomAddress = '0x1234567890123456789012345678901234567890';
const isRandomDelegated = await isEip7702StatelessDelegatedAccount({
client: publicClient,
accountAddress: randomAddress,
});

expect(
isRandomDelegated,
'Random address should not be delegated to EIP7702StatelessDeleGator',
).toBe(false);
});

test('isDeployed() method correctly identifies EIP7702 delegation for Stateless7702 accounts', async () => {
// Test that Alice's smart account's isDeployed() method returns true
// because her EOA is properly delegated to EIP7702StatelessDeleGator
const isAliceDeployed = await aliceSmartAccount.isDeployed();

expect(
isAliceDeployed,
'Alice smart account should report as deployed when properly delegated to EIP7702StatelessDeleGator',
).toBe(true);

// Create a smart account for a non-delegated EOA
const nonDelegatedAccount = privateKeyToAccount(generatePrivateKey());
const client = createClient({ transport, chain });

const nonDelegatedSmartAccount = await toMetaMaskSmartAccount({
client,
implementation: Implementation.Stateless7702,
address: nonDelegatedAccount.address,
signatory: { account: nonDelegatedAccount },
});

// Test that a non-delegated account's isDeployed() method returns false
const isNonDelegatedDeployed = await nonDelegatedSmartAccount.isDeployed();

expect(
isNonDelegatedDeployed,
'Non-delegated smart account should report as not deployed',
).toBe(false);

// Verify that the non-delegated account has no code at all
const code = await publicClient.getCode({
address: nonDelegatedAccount.address,
});
expect(code, 'Non-delegated account should have no code').toBeUndefined();
});

test('isDeployed() returns false for addresses with code that are not delegated to EIP7702StatelessDeleGator', async () => {
// Deploy a regular contract to get an address with code
const counterContract = await deployCounter(aliceSmartAccount.address);

// Verify the contract has code
const contractCode = await publicClient.getCode({
address: counterContract.address,
});
expect(contractCode, 'Contract should have code deployed').toBeDefined();
expect(
contractCode!.length,
'Contract code should not be empty',
).toBeGreaterThan(2); // More than just '0x'

// Create a Stateless7702 smart account pointing to this contract address
const contractAccount = privateKeyToAccount(generatePrivateKey());

const contractSmartAccount = await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Stateless7702,
address: counterContract.address, // Point to the contract address
signatory: { account: contractAccount },
});

// Test that isDeployed() returns false even though there is code at the address
// because the code is not an EIP7702StatelessDeleGator delegation
const isContractDeployed = await contractSmartAccount.isDeployed();

expect(
isContractDeployed,
'Smart account should report as not deployed when address has non-EIP7702StatelessDeleGator code',
).toBe(false);

// Also test with the standalone function to show it would return false too
const { isEip7702StatelessDelegatedAccount } = await import(
'@metamask/delegation-toolkit'
);

const isContractDelegated = await isEip7702StatelessDelegatedAccount({
client: publicClient,
accountAddress: counterContract.address,
});

expect(
isContractDelegated,
'Contract address should not be identified as EIP7702StatelessDeleGator delegation',
).toBe(false);
});
Loading