Skip to content

Commit 8a3c270

Browse files
committed
Add e2e test for approval revocation enforcer
1 parent de1d948 commit 8a3c270

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { beforeEach, expect, test } from 'vitest';
2+
import {
3+
createExecution,
4+
ExecutionMode,
5+
Implementation,
6+
ROOT_AUTHORITY,
7+
toMetaMaskSmartAccount,
8+
} from '@metamask/smart-accounts-kit';
9+
import type {
10+
Delegation,
11+
MetaMaskSmartAccount,
12+
} from '@metamask/smart-accounts-kit';
13+
import {
14+
createCaveatBuilder,
15+
encodeDelegations,
16+
encodeExecutionCalldatas,
17+
} from '@metamask/smart-accounts-kit/utils';
18+
import {
19+
deployErc20Token,
20+
deploySmartAccount,
21+
fundAddressWithErc20Token,
22+
gasPrice,
23+
publicClient,
24+
randomAddress,
25+
sponsoredBundlerClient,
26+
} from '../utils/helpers';
27+
import { encodeFunctionData, parseEther, type Address, type Hex } from 'viem';
28+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
29+
import { expectUserOperationToSucceed } from '../utils/assertions';
30+
import * as ERC20Token from '../../contracts/out/ERC20Token.sol/ERC20Token.json';
31+
32+
const { abi: erc20TokenAbi } = ERC20Token;
33+
34+
let aliceSmartAccount: MetaMaskSmartAccount;
35+
let bobSmartAccount: MetaMaskSmartAccount;
36+
let erc20TokenAddress: Hex;
37+
38+
beforeEach(async () => {
39+
const alice = privateKeyToAccount(generatePrivateKey());
40+
const bob = privateKeyToAccount(generatePrivateKey());
41+
42+
erc20TokenAddress = await deployErc20Token();
43+
44+
aliceSmartAccount = await toMetaMaskSmartAccount({
45+
client: publicClient,
46+
implementation: Implementation.Hybrid,
47+
deployParams: [alice.address, [], [], []],
48+
deploySalt: '0x1',
49+
signer: { account: alice },
50+
});
51+
await deploySmartAccount(aliceSmartAccount);
52+
53+
bobSmartAccount = await toMetaMaskSmartAccount({
54+
client: publicClient,
55+
implementation: Implementation.Hybrid,
56+
deployParams: [bob.address, [], [], []],
57+
deploySalt: '0x1',
58+
signer: { account: bob },
59+
});
60+
61+
await fundAddressWithErc20Token(
62+
aliceSmartAccount.address,
63+
erc20TokenAddress,
64+
parseEther('10'),
65+
);
66+
});
67+
68+
test('maincase: Bob revokes an existing ERC20 approval', async () => {
69+
const spender = randomAddress();
70+
const initialAllowance = parseEther('3');
71+
72+
await setErc20Approval(aliceSmartAccount, spender, initialAllowance);
73+
74+
const allowanceBefore = await getErc20Allowance(
75+
aliceSmartAccount.address,
76+
spender,
77+
);
78+
expect(allowanceBefore).toEqual(initialAllowance);
79+
80+
const delegation: Delegation = {
81+
delegate: bobSmartAccount.address,
82+
delegator: aliceSmartAccount.address,
83+
authority: ROOT_AUTHORITY,
84+
salt: '0x0',
85+
caveats: createCaveatBuilder(aliceSmartAccount.environment)
86+
.addCaveat('approvalRevocation', {
87+
erc20Approve: true,
88+
erc721Approve: false,
89+
erc721SetApprovalForAll: false,
90+
permit2ApproveZero: false,
91+
permit2Lockdown: false,
92+
permit2InvalidateNonces: false,
93+
})
94+
.build(),
95+
signature: '0x',
96+
};
97+
98+
const signedDelegation = {
99+
...delegation,
100+
signature: await aliceSmartAccount.signDelegation({ delegation }),
101+
};
102+
103+
const execution = createExecution({
104+
target: erc20TokenAddress,
105+
callData: encodeFunctionData({
106+
abi: erc20TokenAbi,
107+
functionName: 'approve',
108+
args: [spender, 0n],
109+
}),
110+
});
111+
112+
const redeemData = encodeFunctionData({
113+
abi: bobSmartAccount.abi,
114+
functionName: 'redeemDelegations',
115+
args: [
116+
[encodeDelegations([signedDelegation])],
117+
[ExecutionMode.SingleDefault],
118+
encodeExecutionCalldatas([[execution]]),
119+
],
120+
});
121+
122+
const userOpHash = await sponsoredBundlerClient.sendUserOperation({
123+
account: bobSmartAccount,
124+
calls: [
125+
{
126+
to: bobSmartAccount.address,
127+
data: redeemData,
128+
},
129+
],
130+
...gasPrice,
131+
});
132+
133+
const receipt = await sponsoredBundlerClient.waitForUserOperationReceipt({
134+
hash: userOpHash,
135+
});
136+
expectUserOperationToSucceed(receipt);
137+
138+
const allowanceAfter = await getErc20Allowance(
139+
aliceSmartAccount.address,
140+
spender,
141+
);
142+
expect(allowanceAfter).toEqual(0n);
143+
});
144+
145+
const setErc20Approval = async (
146+
owner: MetaMaskSmartAccount<Implementation>,
147+
spender: Address,
148+
amount: bigint,
149+
) => {
150+
const approveCallData = encodeFunctionData({
151+
abi: erc20TokenAbi,
152+
functionName: 'approve',
153+
args: [spender, amount],
154+
});
155+
156+
const userOpHash = await sponsoredBundlerClient.sendUserOperation({
157+
account: owner,
158+
calls: [
159+
{
160+
to: erc20TokenAddress,
161+
data: approveCallData,
162+
},
163+
],
164+
...gasPrice,
165+
});
166+
167+
const receipt = await sponsoredBundlerClient.waitForUserOperationReceipt({
168+
hash: userOpHash,
169+
});
170+
expectUserOperationToSucceed(receipt);
171+
};
172+
173+
const getErc20Allowance = async (
174+
owner: Address,
175+
spender: Address,
176+
): Promise<bigint> => {
177+
const result = await publicClient.readContract({
178+
address: erc20TokenAddress,
179+
abi: erc20TokenAbi,
180+
functionName: 'allowance',
181+
args: [owner, spender],
182+
});
183+
184+
if (typeof result !== 'bigint') {
185+
throw new Error('Result is not a bigint');
186+
}
187+
188+
return result;
189+
};

0 commit comments

Comments
 (0)