Skip to content
This repository was archived by the owner on Feb 8, 2025. It is now read-only.

Commit c4cfd1b

Browse files
authored
Merge pull request #290 from zallo-labs/stable-activation
feat(api): stable deployment account deployment addresses when proxy …
2 parents 3fbabbc + 8f6ed00 commit c4cfd1b

18 files changed

+85
-146
lines changed

api/dbschema/bootstrap.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ configure instance set effective_io_concurrency := 1000;
99
configure instance set query_work_mem := <cfg::memory>"8MiB";
1010

1111
# Memory available to cache data - 50%
12-
configure instance set shared_buffers := <cfg::memory>"2GiB";
12+
configure instance set shared_buffers := <cfg::memory>"4GiB";
1313

1414
# Total memory available to the database for caching - 75%
1515
configure instance set effective_cache_size := <cfg::memory>"6GiB";

api/dbschema/default.esdl

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module default {
77
type Account extending Labelled {
88
overloaded required address: UAddress { constraint exclusive; }
99
required implementation: Address;
10-
required salt: Bytes32;
10+
required initialization: tuple<salt: Bytes32, bytecodeHash: Bytes32, aaVersion: uint16>;
1111
activationEthFee: decimal { constraint min_value(0); }
1212
upgradedAtBlock: bigint { constraint min_value(0); }
1313
photo: Url;

api/dbschema/edgeql-js/__spec__.ts

+2-1
Large diffs are not rendered by default.

api/dbschema/edgeql-js/modules/default.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ export type $AccountλShape = $.typeutil.flatten<Omit<$LabelledλShape, "address
9696
"active": $.PropertyDesc<_std.$bool, $.Cardinality.One, false, true, false, false>;
9797
"implementation": $.PropertyDesc<$Address, $.Cardinality.One, false, false, false, false>;
9898
"photo": $.PropertyDesc<$Url, $.Cardinality.AtMostOne, false, false, false, false>;
99-
"salt": $.PropertyDesc<$Bytes32, $.Cardinality.One, false, false, false, false>;
10099
"policies": $.LinkDesc<$Policy, $.Cardinality.Many, {}, false, true, false, false>;
101100
"approvers": $.LinkDesc<$Approver, $.Cardinality.Many, {}, false, true, false, false>;
102101
"messages": $.LinkDesc<$Message, $.Cardinality.Many, {}, false, true, false, false>;
103102
"proposals": $.LinkDesc<$Proposal, $.Cardinality.Many, {}, false, true, false, false>;
104103
"transactions": $.LinkDesc<$Transaction, $.Cardinality.Many, {}, false, true, false, false>;
105104
"transfers": $.LinkDesc<$Transfer, $.Cardinality.Many, {}, false, true, false, false>;
105+
"initialization": $.PropertyDesc<$.NamedTupleType<{salt: $Bytes32, bytecodeHash: $Bytes32, aaVersion: $uint16}>, $.Cardinality.One, false, false, false, false>;
106106
"<account[is Proposal]": $.LinkDesc<$Proposal, $.Cardinality.Many, {}, false, false, false, false>;
107107
"<account[is Message]": $.LinkDesc<$Message, $.Cardinality.Many, {}, false, false, false, false>;
108108
"<account[is Transaction]": $.LinkDesc<$Transaction, $.Cardinality.Many, {}, false, false, false, false>;

api/dbschema/interfaces.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@ export namespace $default {
8787
"active": boolean;
8888
"implementation": string;
8989
"photo"?: string | null;
90-
"salt": string;
9190
"policies": Policy[];
9291
"approvers": Approver[];
9392
"messages": Message[];
9493
"proposals": Proposal[];
9594
"transactions": Transaction[];
9695
"transfers": Transfer[];
96+
"initialization": {salt: string, bytecodeHash: string, aaVersion: number};
9797
}
9898
export interface Action extends std.$Object {
9999
"functions": ActionFunction[];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
CREATE MIGRATION m13y2dtmcrd7yvmorrgn62mp2cakpvrcbz7p7g2rr4wk34b6ow7m7q
2+
ONTO m174x3tft7rqwxzekegp7qad4qlscof7tojfcmw4wielhp7h4mqsya
3+
{
4+
ALTER TYPE default::Account {
5+
CREATE REQUIRED PROPERTY initialization: tuple<salt: default::Bytes32, bytecodeHash: default::Bytes32, aaVersion: default::uint16> {
6+
SET REQUIRED USING ((
7+
salt := .salt,
8+
bytecodeHash := '0x0100007b3eebe76a9052ad76c1efe68151404a98aee77b96cbdbc62df0660b27',
9+
aaVersion := 1
10+
));
11+
};
12+
};
13+
ALTER TYPE default::Account {
14+
DROP PROPERTY salt;
15+
};
16+
};

api/src/feat/accounts/accounts.service.spec.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { createMock, DeepMocked } from '@golevelup/ts-jest';
22
import { Test } from '@nestjs/testing';
33
import { UserContext, asUser, getUserCtx } from '~/core/context';
4-
import { randomLabel, randomAddress, randomUAddress, randomUser } from '~/util/test';
5-
import { getProxyAddress, UAddress } from 'lib';
4+
import { randomAddress, randomLabel, randomUAddress, randomUser } from '~/util/test';
5+
import { UAddress } from 'lib';
66
import { PoliciesService } from '../policies/policies.service';
77
import { BullModule, getQueueToken } from '@nestjs/bullmq';
88
import { ActivationsQueue } from '../activations/activations.queue';
@@ -12,13 +12,14 @@ import e from '~/edgeql-js';
1212
import { uuid } from 'edgedb/dist/codecs/ifaces';
1313
import { AccountsCacheService } from '../auth/accounts.cache.service';
1414
import { TypedQueue } from '~/core/bull/bull.util';
15+
import { create2Address } from 'zksync-ethers/build/utils';
1516

16-
jest.mock('lib', () => ({
17-
...jest.requireActual('lib'),
18-
getProxyAddress: jest.fn(),
17+
jest.mock('zksync-ethers/build/utils', () => ({
18+
...jest.requireActual('zksync-ethers/build/utils'),
19+
create2Address: jest.fn(),
1920
}));
2021

21-
const getProxyAddressMock = jest.mocked(getProxyAddress);
22+
const create2AddressMock = jest.mocked(create2Address);
2223

2324
describe(AccountsService.name, () => {
2425
let service: AccountsService;
@@ -51,7 +52,7 @@ describe(AccountsService.name, () => {
5152
const createAccount = async () => {
5253
const userCtx = getUserCtx();
5354

54-
getProxyAddressMock.mockReturnValue(randomAddress());
55+
create2AddressMock.mockReturnValue(randomAddress());
5556

5657
return service.createAccount({
5758
chain: 'zksync-local',

api/src/feat/accounts/accounts.service.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import e from '~/edgeql-js';
44
import {
55
asPolicyKey,
66
randomDeploySalt,
7-
getProxyAddress,
87
Address,
98
UAddress,
109
asAddress,
1110
ACCOUNT_IMPLEMENTATION,
1211
asUAddress,
1312
PLACEHOLDER_ACCOUNT_ADDRESS,
13+
ACCOUNT_PROXY,
14+
encodeProxyConstructorArgs,
1415
} from 'lib';
1516
import { CREATE2_FACTORY } from 'lib/dapps';
1617
import { ShapeFunc } from '~/core/database';
@@ -32,6 +33,8 @@ import { v4 as uuid } from 'uuid';
3233
import { selectAccount2 } from './accounts.util';
3334
import { AccountEvent } from './accounts.model';
3435
import { PolicyInput } from '../policies/policies.input';
36+
import { utils as zkUtils } from 'zksync-ethers';
37+
import { toHex } from 'viem';
3538

3639
const accountTrigger = (account: UAddress) => `account.updated:${account}`;
3740
const accountApproverTrigger = (approver: Address) => `account.updated:approver:${approver}`;
@@ -107,13 +110,17 @@ export class AccountsService {
107110
throw new UserInputError('Duplicate policy keys');
108111

109112
const implementation = ACCOUNT_IMPLEMENTATION.address[chain];
113+
const bytecodeHash = toHex(zkUtils.hashBytecode(ACCOUNT_PROXY.bytecode));
110114
const address = asUAddress(
111-
getProxyAddress({
112-
deployer: CREATE2_FACTORY.address,
113-
implementation,
115+
zkUtils.create2Address(
116+
CREATE2_FACTORY.address,
117+
bytecodeHash,
114118
salt,
115-
policies: policies.map((p) => inputAsPolicy(p.key, p)),
116-
}),
119+
encodeProxyConstructorArgs({
120+
implementation,
121+
policies: policies.map((p) => inputAsPolicy(p.key, p)),
122+
}),
123+
),
117124
chain,
118125
);
119126

@@ -147,7 +154,7 @@ export class AccountsService {
147154
address,
148155
name,
149156
implementation,
150-
salt,
157+
initialization: { salt, bytecodeHash, aaVersion: 1 },
151158
}),
152159
);
153160

api/src/feat/accounts/insert-account.edgeql

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ insert Account {
33
address := <UAddress>$address,
44
name := <str>$name,
55
implementation := <Address>$implementation,
6-
salt := <Bytes32>$salt
6+
initialization := <tuple<salt: Bytes32, bytecodeHash: Bytes32, aaVersion: uint16>>$initialization
77
}

api/src/feat/accounts/insert-account.query.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ export type InsertAccountArgs = {
77
readonly "address": string;
88
readonly "name": string;
99
readonly "implementation": string;
10-
readonly "salt": string;
10+
readonly "initialization": {
11+
readonly "salt": string;
12+
readonly "bytecodeHash": string;
13+
readonly "aaVersion": number;
14+
};
1115
};
1216

1317
export type InsertAccountReturns = {
@@ -21,7 +25,7 @@ insert Account {
2125
address := <UAddress>$address,
2226
name := <str>$name,
2327
implementation := <Address>$implementation,
24-
salt := <Bytes32>$salt
28+
initialization := <tuple<salt: Bytes32, bytecodeHash: Bytes32, aaVersion: uint16>>$initialization
2529
}`, args);
2630

2731
}

api/src/feat/activations/activations.service.ts

+12-17
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
replaceSelfAddress,
1010
PLACEHOLDER_ACCOUNT_ADDRESS,
1111
UUID,
12-
ACCOUNT_PROXY,
1312
encodeProxyConstructorArgs,
1413
} from 'lib';
1514
import { CREATE2_FACTORY } from 'lib/dapps';
@@ -21,13 +20,6 @@ import { FlowJob } from 'bullmq';
2120
import { ConfirmationQueue } from '../system-txs/confirmations.queue';
2221
import Decimal from 'decimal.js';
2322
import { SimulationsQueue } from '../simulations/simulations.worker';
24-
import { toHex } from 'viem';
25-
import { utils as zkUtils } from 'zksync-ethers';
26-
27-
interface FeeParams {
28-
account: UAddress;
29-
feePerGas: Decimal;
30-
}
3123

3224
@Injectable()
3325
export class ActivationsService {
@@ -61,15 +53,15 @@ export class ActivationsService {
6153
} satisfies FlowJob;
6254
}
6355

64-
async fee({ account, feePerGas }: FeeParams): Promise<Decimal | null> {
65-
const a = await this.db.queryWith(
56+
async fee(account: UAddress): Promise<Decimal | null> {
57+
const a = await this.db.queryWith2(
6658
{ address: e.UAddress },
59+
{ address: account },
6760
({ address }) =>
6861
e.select(e.Account, () => ({
6962
filter_single: { address },
7063
activationEthFee: true,
7164
})),
72-
{ address: account },
7365
);
7466
if (!a) return null;
7567
if (a.activationEthFee) return new Decimal(a.activationEthFee);
@@ -79,8 +71,11 @@ export class ActivationsService {
7971

8072
const network = this.networks.get(account);
8173
try {
82-
const gas = await network.estimateContractGas(request);
83-
return feePerGas.mul(gas.toString());
74+
const [gas, maxFeePerGas] = await Promise.all([
75+
network.estimateContractGas(request),
76+
network.maxFeePerGas(),
77+
]);
78+
return maxFeePerGas.mul(gas.toString());
8479
} catch (e) {
8580
const isDeployed = !!(await network.getCode({ address: asAddress(account) }))?.length;
8681
if (isDeployed) return null;
@@ -97,7 +92,7 @@ export class ActivationsService {
9792
filter_single: { address },
9893
active: true,
9994
implementation: true,
100-
salt: true,
95+
initialization: true,
10196
initPolicies: e.select(a.policies, (p) => ({
10297
filter: p.initState,
10398
...PolicyShape,
@@ -126,10 +121,10 @@ export class ActivationsService {
126121
address: CREATE2_FACTORY.address,
127122
functionName: 'create2Account' as const,
128123
args: [
129-
asHex(account.salt),
130-
toHex(zkUtils.hashBytecode(ACCOUNT_PROXY.bytecode)),
124+
asHex(account.initialization.salt),
125+
asHex(account.initialization.bytecodeHash),
131126
constructorArgs,
132-
1, // AccountAbstractionVersion.Version1
127+
account.initialization.aaVersion,
133128
] as const,
134129
// factoryDeps: [ACCOUNT_PROXY.bytecode], // Throws "rpc method is not whitelisted" if provided; bytecode must be deployed using SystemContractDeployer first
135130
// gas: 3_000_000n * BigInt(params.policies.length), // ~1M per policy; gas estimation panics if not provided

api/src/feat/activations/activations.worker.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ActivationsQueue } from './activations.queue';
88
import { DatabaseService } from '~/core/database';
99
import e from '~/edgeql-js';
1010
import { selectTransaction2 } from '../transactions/transactions.util';
11+
import { UnrecoverableError } from 'bullmq';
12+
import { asAddress } from 'lib';
1113

1214
@Injectable()
1315
@Processor(ActivationsQueue.name, { autorun: false })
@@ -42,7 +44,11 @@ export class ActivationsWorker extends Worker<ActivationsQueue> {
4244
const request = await this.activations.request(account);
4345
if (!request) return null;
4446

45-
await network.simulateContract(request); // Throws on error
47+
const sim = await network.simulateContract(request); // Throws on error
48+
if (sim.result !== asAddress(account))
49+
throw new UnrecoverableError(
50+
`Simulated deployment address ${sim.result} doesn't match expected address ${asAddress(account)}`,
51+
);
4652

4753
const { account: _, ...req } = request;
4854
const receipt = await network.useWallet((wallet) => wallet.writeContract(req));

api/src/feat/paymasters/paymasters.service.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ export class PaymastersService implements OnModuleInit {
3939
}
4040

4141
async paymasterFees({ account }: PaymasterFeesParams): Promise<PaymasterFeeParts> {
42-
const feePerGas = await this.networks.get(account).maxFeePerGas();
43-
44-
const activation = await this.activations.fee({ account, feePerGas });
42+
const activation = await this.activations.fee(account);
4543

4644
return { activation: activation ?? new Decimal(0) };
4745
}

api/src/feat/policies/policies.service.spec.ts

+4-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
import { Test } from '@nestjs/testing';
22
import { createMock, DeepMocked } from '@golevelup/ts-jest';
33
import { PoliciesService, ProposePoliciesParams } from './policies.service';
4-
import {
5-
asPolicyKey,
6-
asSelector,
7-
asUUID,
8-
randomDeploySalt,
9-
randomHex,
10-
UAddress,
11-
ZERO_ADDR,
12-
} from 'lib';
4+
import { asPolicyKey, asSelector, asUUID, randomHex, UAddress, ZERO_ADDR } from 'lib';
135
import { UserContext } from '~/core/context';
146
import { asUser, getUserCtx } from '~/core/context';
157
import { randomAddress, randomLabel, randomUAddress, randomUser } from '~/util/test';
@@ -20,6 +12,7 @@ import { inputAsPolicy, policyStateAsPolicy, PolicyShape, selectPolicy } from '.
2012
import { PolicyInput } from './policies.input';
2113
import { v1 as uuidv1 } from 'uuid';
2214
import { selectAccount } from '../accounts/accounts.util';
15+
import { zeroHash } from 'viem';
2316

2417
describe(PoliciesService.name, () => {
2518
let service: PoliciesService;
@@ -58,8 +51,8 @@ describe(PoliciesService.name, () => {
5851
id: accountId,
5952
address: account,
6053
name: randomLabel(),
61-
implementation: randomAddress(),
62-
salt: randomDeploySalt(),
54+
implementation: ZERO_ADDR,
55+
initialization: { salt: zeroHash, bytecodeHash: zeroHash, aaVersion: 1 },
6356
}),
6457
);
6558

api/src/feat/transactions/transactions.service.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Test } from '@nestjs/testing';
22
import { createMock, DeepMocked } from '@golevelup/ts-jest';
33
import { asUser, getUserCtx, UserContext } from '~/core/context';
44
import { DeepPartial, randomAddress, randomLabel, randomUAddress, randomUser } from '~/util/test';
5-
import { randomDeploySalt, Hex, UAddress, ZERO_ADDR, asUUID, asPolicyKey } from 'lib';
5+
import { Hex, UAddress, ZERO_ADDR, asUUID, asPolicyKey } from 'lib';
66
import { Network, NetworksService } from '~/core/networks/networks.service';
77
import { ProposeTransactionInput } from './transactions.input';
88
import { DatabaseService } from '~/core/database';
@@ -112,8 +112,8 @@ describe(TransactionsService.name, () => {
112112
id: accountId,
113113
address: account,
114114
name: randomLabel(),
115-
implementation: randomAddress(),
116-
salt: randomDeploySalt(),
115+
implementation: ZERO_ADDR,
116+
initialization: { salt: zeroHash, bytecodeHash: zeroHash, aaVersion: 1 },
117117
upgradedAtBlock: 1n,
118118
})
119119
.unlessConflict(),

api/src/feat/transfers/transfers.service.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import {
1313
asChain,
1414
asUAddress,
1515
asUUID,
16-
randomDeploySalt,
1716
} from 'lib';
18-
import { randomAddress, randomLabel, randomUAddress, randomUser } from '~/util/test';
17+
import { randomLabel, randomUAddress, randomUser } from '~/util/test';
1918
import e from '~/edgeql-js';
2019
import { v1 as uuidv1 } from 'uuid';
2120
import { InsertShape } from '~/edgeql-js/insert';
2221
import { $Transfer } from '~/edgeql-js/modules/default';
22+
import { zeroHash } from 'viem';
2323

2424
describe(TransfersService.name, () => {
2525
let service: TransfersService;
@@ -45,8 +45,8 @@ describe(TransfersService.name, () => {
4545
id,
4646
address,
4747
name: randomLabel(),
48-
implementation: randomAddress(),
49-
salt: randomDeploySalt(),
48+
implementation: ZERO_ADDR,
49+
initialization: { salt: zeroHash, bytecodeHash: zeroHash, aaVersion: 1 },
5050
upgradedAtBlock: 1n,
5151
})
5252
.unlessConflict()

contracts/script/deployProxy.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TASK_COMPILE } from 'hardhat/builtin-tasks/task-names';
44
import AccountProxy from './contracts/AccountProxy';
55
import { deploy } from './util/deploy';
66

7-
const ACCOUNT = '0xc0c1c0692F1aCd4FA91ccc73fB9aCFCd60Dd571a';
7+
const ACCOUNT = '0x696532D64a358a4CC2eCDBE698a4a08c7841af8c';
88

99
// Deploys an uninitialized proxy
1010
// Used for first-time proxy bytecode deployment

0 commit comments

Comments
 (0)