Skip to content

Commit e3c80c5

Browse files
penovicpkfastovPhilippeR26
authored
feat: implement SNIP-9 outside execution functionality (#1208)
* feat: draft SNIP-9 implementation (#1111) * feat: finalize SNIP-9 outside execution implementation (#1202) --------- Co-authored-by: Konstantin Fastov <kfastov@gmail.com> Co-authored-by: Philippe ROSTAN <81040730+PhilippeR26@users.noreply.github.com>
1 parent bdad9a5 commit e3c80c5

File tree

13 files changed

+38993
-2
lines changed

13 files changed

+38993
-2
lines changed
File renamed without changes.

__mocks__/cairo/account/accountArgent040.casm

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

__mocks__/cairo/account/accountArgent040.json

Lines changed: 37885 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
// We test here the most common case: an account compatible with ERC-165 and SNIP-9 (v2).
2+
// To limit test duration, these cases are not tested: non ERC165 account, non SNIP-9 account, SNIP9-v1 account.
3+
import {
4+
Provider,
5+
Account,
6+
cairo,
7+
ec,
8+
stark,
9+
CairoCustomEnum,
10+
CairoOption,
11+
CairoOptionVariant,
12+
CallData,
13+
OutsideExecutionVersion,
14+
type OutsideExecutionOptions,
15+
type OutsideTransaction,
16+
constants,
17+
type Call,
18+
Contract,
19+
outsideExecution,
20+
type TypedData,
21+
type Calldata,
22+
src5,
23+
} from '../src';
24+
import { getSelectorFromName } from '../src/utils/hash';
25+
import { getDecimalString } from '../src/utils/num';
26+
import {
27+
compiledArgentX4Account,
28+
compiledErc20OZ,
29+
getTestAccount,
30+
getTestProvider,
31+
} from './config/fixtures';
32+
33+
describe('Account and OutsideExecution', () => {
34+
const ethAddress = '0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7';
35+
const provider = new Provider(getTestProvider());
36+
const executorAccount = getTestAccount(provider);
37+
let signerAccount: Account;
38+
const targetPK = stark.randomAddress();
39+
const targetPubK = ec.starkCurve.getStarkKey(targetPK);
40+
// For ERC20 transfer outside call
41+
const recipientAccount = executorAccount;
42+
const ethContract = new Contract(compiledErc20OZ.abi, ethAddress, provider);
43+
44+
beforeAll(async () => {
45+
// Deploy the SNIP-9 signer account (ArgentX v 0.4.0, using SNIP-9 v2):
46+
const calldataAX = new CallData(compiledArgentX4Account.abi);
47+
const axSigner = new CairoCustomEnum({ Starknet: { pubkey: targetPubK } });
48+
const axGuardian = new CairoOption<unknown>(CairoOptionVariant.None);
49+
const constructorAXCallData = calldataAX.compile('constructor', {
50+
owner: axSigner,
51+
guardian: axGuardian,
52+
});
53+
const response = await executorAccount.declareAndDeploy({
54+
contract: compiledArgentX4Account,
55+
classHash: '0x36078334509b514626504edc9fb252328d1a240e4e948bef8d0c08dff45927f',
56+
compiledClassHash: '0x7a663375245780bd307f56fde688e33e5c260ab02b76741a57711c5b60d47f6',
57+
constructorCalldata: constructorAXCallData,
58+
});
59+
const targetAddress = response.deploy.contract_address;
60+
signerAccount = new Account(provider, targetAddress, targetPK);
61+
62+
// Transfer dust of ETH token to the signer account
63+
const transferCall = {
64+
contractAddress: ethAddress,
65+
entrypoint: 'transfer',
66+
calldata: {
67+
recipient: signerAccount.address,
68+
amount: cairo.uint256(1000),
69+
},
70+
};
71+
const { transaction_hash } = await executorAccount.execute(transferCall);
72+
await provider.waitForTransaction(transaction_hash);
73+
});
74+
75+
test('getOutsideCall', async () => {
76+
const call1: Call = {
77+
contractAddress: '0x0123',
78+
entrypoint: 'transfer',
79+
calldata: {
80+
recipient: '0xabcd',
81+
amount: cairo.uint256(10),
82+
},
83+
};
84+
expect(outsideExecution.getOutsideCall(call1)).toEqual({
85+
to: '0x0123',
86+
selector: getSelectorFromName(call1.entrypoint),
87+
calldata: ['43981', '10', '0'],
88+
});
89+
});
90+
91+
test('Build SNIP-9 v2 TypedData', async () => {
92+
const call1: Call = {
93+
contractAddress: '0x0123',
94+
entrypoint: 'transfer',
95+
calldata: {
96+
recipient: '0xabcd',
97+
amount: cairo.uint256(10),
98+
},
99+
};
100+
const callOptions: OutsideExecutionOptions = {
101+
caller: '0x1234',
102+
execute_after: 100,
103+
execute_before: 200,
104+
};
105+
const message: TypedData = outsideExecution.getTypedData(
106+
constants.StarknetChainId.SN_SEPOLIA,
107+
callOptions,
108+
21,
109+
[call1],
110+
OutsideExecutionVersion.V2
111+
);
112+
expect(message).toEqual({
113+
domain: {
114+
chainId: '0x534e5f5345504f4c4941',
115+
name: 'Account.execute_from_outside',
116+
revision: '1',
117+
version: '2',
118+
},
119+
message: {
120+
Caller: '0x1234',
121+
Calls: [
122+
{
123+
Calldata: ['43981', '10', '0'],
124+
Selector: '0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e',
125+
To: '0x0123',
126+
},
127+
],
128+
'Execute After': 100,
129+
'Execute Before': 200,
130+
Nonce: 21,
131+
},
132+
primaryType: 'OutsideExecution',
133+
types: {
134+
Call: [
135+
{
136+
name: 'To',
137+
type: 'ContractAddress',
138+
},
139+
{
140+
name: 'Selector',
141+
type: 'selector',
142+
},
143+
{
144+
name: 'Calldata',
145+
type: 'felt*',
146+
},
147+
],
148+
OutsideExecution: [
149+
{
150+
name: 'Caller',
151+
type: 'ContractAddress',
152+
},
153+
{
154+
name: 'Nonce',
155+
type: 'felt',
156+
},
157+
{
158+
name: 'Execute After',
159+
type: 'u128',
160+
},
161+
{
162+
name: 'Execute Before',
163+
type: 'u128',
164+
},
165+
{
166+
name: 'Calls',
167+
type: 'Call*',
168+
},
169+
],
170+
StarknetDomain: [
171+
{
172+
name: 'name',
173+
type: 'shortstring',
174+
},
175+
{
176+
name: 'version',
177+
type: 'shortstring',
178+
},
179+
{
180+
name: 'chainId',
181+
type: 'shortstring',
182+
},
183+
{
184+
name: 'revision',
185+
type: 'shortstring',
186+
},
187+
],
188+
},
189+
});
190+
});
191+
192+
test('buildExecuteFromOutsideCallData', async () => {
193+
const outsideTransaction: OutsideTransaction = {
194+
outsideExecution: {
195+
caller: '0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691',
196+
nonce: '0x7d0b4b4fce4b236e63d2bb5fc321935d52935cd3b268248cf9cf29c496bd0ae',
197+
execute_after: 500,
198+
execute_before: 600,
199+
calls: [{ to: '0x678', selector: '0x890', calldata: [12, 13] }],
200+
},
201+
signature: ['0x123', '0x456'],
202+
signerAddress: '0x3b278ebae434f283f9340587a7f2dd4282658ac8e03cb9b0956db23a0a83657',
203+
version: OutsideExecutionVersion.V2,
204+
};
205+
206+
const execute: Calldata = outsideExecution.buildExecuteFromOutsideCallData(outsideTransaction);
207+
expect(execute).toEqual([
208+
'2846891009026995430665703316224827616914889274105712248413538305735679628945',
209+
'3534941323322368687588030484849371698982661160919690922146419787802417549486',
210+
'500',
211+
'600',
212+
'1',
213+
'1656',
214+
'2192',
215+
'2',
216+
'12',
217+
'13',
218+
'2',
219+
'291',
220+
'1110',
221+
]);
222+
});
223+
224+
test('Signer account should support SNIP-9 v2', async () => {
225+
expect(await signerAccount.getSnip9Version()).toBe(OutsideExecutionVersion.V2);
226+
});
227+
228+
test('SNIP-9 nonce', async () => {
229+
const nonce = await signerAccount.getSnip9Nonce();
230+
expect(nonce).toBeDefined();
231+
expect(await signerAccount.isValidSnip9Nonce(nonce)).toBe(true);
232+
});
233+
234+
test('should build and execute outside transactions', async () => {
235+
const now_seconds = Math.floor(Date.now() / 1000);
236+
const hour_ago = (now_seconds - 3600).toString();
237+
const hour_later = (now_seconds + 3600).toString();
238+
const callOptions: OutsideExecutionOptions = {
239+
caller: executorAccount.address,
240+
execute_after: hour_ago,
241+
execute_before: hour_later,
242+
};
243+
const callOptions4: OutsideExecutionOptions = {
244+
...callOptions,
245+
caller: 'ANY_CALLER',
246+
};
247+
const call1: Call = {
248+
contractAddress: ethAddress,
249+
entrypoint: 'transfer',
250+
calldata: {
251+
recipient: recipientAccount.address,
252+
amount: cairo.uint256(100),
253+
},
254+
};
255+
const call2: Call = {
256+
contractAddress: ethAddress,
257+
entrypoint: 'transfer',
258+
calldata: {
259+
recipient: recipientAccount.address,
260+
amount: cairo.uint256(200),
261+
},
262+
};
263+
const call3: Call = {
264+
contractAddress: ethAddress,
265+
entrypoint: 'transfer',
266+
calldata: {
267+
recipient: recipientAccount.address,
268+
amount: cairo.uint256(300),
269+
},
270+
};
271+
const call4: Call = {
272+
contractAddress: ethAddress,
273+
entrypoint: 'transfer',
274+
calldata: {
275+
recipient: recipientAccount.address,
276+
amount: cairo.uint256(400),
277+
},
278+
};
279+
const outsideTransaction3: OutsideTransaction = await signerAccount.getOutsideTransaction(
280+
callOptions4,
281+
call4
282+
); // ANY_CALLER
283+
284+
const outsideTransaction1: OutsideTransaction = await signerAccount.getOutsideTransaction(
285+
callOptions,
286+
call3
287+
); // designated caller
288+
expect(outsideTransaction3.version).toBe(OutsideExecutionVersion.V2);
289+
expect(outsideTransaction1.signerAddress).toBe(signerAccount.address);
290+
expect(outsideTransaction3.outsideExecution.caller).toBe(constants.OutsideExecutionCallerAny);
291+
expect(outsideTransaction1.outsideExecution.caller).toBe(executorAccount.address);
292+
expect(outsideTransaction1.outsideExecution.execute_after).toBe(hour_ago);
293+
expect(outsideTransaction1.outsideExecution.execute_before).toBe(hour_later);
294+
expect(outsideTransaction1.outsideExecution.calls).toEqual([
295+
{
296+
to: ethAddress,
297+
selector: '0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e',
298+
calldata: [getDecimalString(recipientAccount.address), '300', '0'],
299+
},
300+
]);
301+
// get outside transaction of a multiCall :
302+
const outsideTransaction2: OutsideTransaction = await signerAccount.getOutsideTransaction(
303+
callOptions,
304+
[call1, call2]
305+
);
306+
expect(outsideTransaction2.outsideExecution.calls).toEqual([
307+
{
308+
to: ethAddress,
309+
selector: '0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e',
310+
calldata: [getDecimalString(recipientAccount.address), '100', '0'],
311+
},
312+
{
313+
to: ethAddress,
314+
selector: '0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e',
315+
calldata: [getDecimalString(recipientAccount.address), '200', '0'],
316+
},
317+
]);
318+
const bal0 = (await ethContract.balanceOf(signerAccount.address)) as bigint;
319+
const res0 = await executorAccount.executeFromOutside(outsideTransaction2);
320+
await provider.waitForTransaction(res0.transaction_hash);
321+
const bal1 = (await ethContract.balanceOf(signerAccount.address)) as bigint;
322+
expect(bal0 - bal1).toBe(300n);
323+
// execute multi outside transactions
324+
const res1 = await executorAccount.executeFromOutside([
325+
outsideTransaction1,
326+
outsideTransaction3,
327+
]);
328+
await provider.waitForTransaction(res1.transaction_hash);
329+
const bal2 = (await ethContract.balanceOf(signerAccount.address)) as bigint;
330+
expect(bal1 - bal2).toBe(700n);
331+
expect(await signerAccount.isValidSnip9Nonce(outsideTransaction3.outsideExecution.nonce)).toBe(
332+
false
333+
);
334+
});
335+
336+
test('ERC165 introspection', async () => {
337+
const isSNIP9 = await src5.supportsInterface(
338+
provider,
339+
signerAccount.address,
340+
constants.SNIP9_V2_INTERFACE_ID
341+
);
342+
expect(isSNIP9).toBe(true);
343+
});
344+
});

__tests__/config/fixtures.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const readContractSierra = (name: string): CompiledSierra =>
3030
export const compiledOpenZeppelinAccount = readContract('Account');
3131
export const compiledErc20 = readContract('ERC20');
3232
export const compiledErc20Echo = readContract('ERC20-echo');
33+
export const compiledErc20OZ = readContractSierra('cairo/ERC20-241/ERC20OZ081.sierra');
34+
export const compiledErc20OZCasm = readContractSierraCasm('cairo/ERC20-241/ERC20OZ081');
3335
export const compiledL1L2 = readContract('l1l2_compiled');
3436
export const compiledTypeTransformation = readContract('contract');
3537
export const compiledMulticall = readContract('multicall');
@@ -41,6 +43,8 @@ export const compiledHelloSierraCasm = readContractSierraCasm('cairo/helloSierra
4143
export const compiledComplexSierra = readContractSierra('cairo/complexInput/complexInput');
4244
export const compiledC1Account = readContractSierra('cairo/account/accountOZ080');
4345
export const compiledC1AccountCasm = readContractSierraCasm('cairo/account/accountOZ080');
46+
export const compiledArgentX4Account = readContractSierra('cairo/account/accountArgent040');
47+
export const compiledArgentX4AccountCasm = readContractSierraCasm('cairo/account/accountArgent040');
4448
export const compiledC1v2 = readContractSierra('cairo/helloCairo2/compiled');
4549
export const compiledC1v2Casm = readContractSierraCasm('cairo/helloCairo2/compiled');
4650
export const compiledC210 = readContractSierra('cairo/cairo210/cairo210.sierra');

0 commit comments

Comments
 (0)