Skip to content

Commit

Permalink
feat(api): stable deployment account deployment addresses when proxy …
Browse files Browse the repository at this point in the history
…bytecode has changed
  • Loading branch information
hbriese committed Aug 21, 2024
1 parent 3fbabbc commit 8f6ed00
Show file tree
Hide file tree
Showing 18 changed files with 85 additions and 146 deletions.
2 changes: 1 addition & 1 deletion api/dbschema/bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ configure instance set effective_io_concurrency := 1000;
configure instance set query_work_mem := <cfg::memory>"8MiB";

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

# Total memory available to the database for caching - 75%
configure instance set effective_cache_size := <cfg::memory>"6GiB";
2 changes: 1 addition & 1 deletion api/dbschema/default.esdl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module default {
type Account extending Labelled {
overloaded required address: UAddress { constraint exclusive; }
required implementation: Address;
required salt: Bytes32;
required initialization: tuple<salt: Bytes32, bytecodeHash: Bytes32, aaVersion: uint16>;
activationEthFee: decimal { constraint min_value(0); }
upgradedAtBlock: bigint { constraint min_value(0); }
photo: Url;
Expand Down
3 changes: 2 additions & 1 deletion api/dbschema/edgeql-js/__spec__.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion api/dbschema/edgeql-js/modules/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ export type $AccountλShape = $.typeutil.flatten<Omit<$LabelledλShape, "address
"active": $.PropertyDesc<_std.$bool, $.Cardinality.One, false, true, false, false>;
"implementation": $.PropertyDesc<$Address, $.Cardinality.One, false, false, false, false>;
"photo": $.PropertyDesc<$Url, $.Cardinality.AtMostOne, false, false, false, false>;
"salt": $.PropertyDesc<$Bytes32, $.Cardinality.One, false, false, false, false>;
"policies": $.LinkDesc<$Policy, $.Cardinality.Many, {}, false, true, false, false>;
"approvers": $.LinkDesc<$Approver, $.Cardinality.Many, {}, false, true, false, false>;
"messages": $.LinkDesc<$Message, $.Cardinality.Many, {}, false, true, false, false>;
"proposals": $.LinkDesc<$Proposal, $.Cardinality.Many, {}, false, true, false, false>;
"transactions": $.LinkDesc<$Transaction, $.Cardinality.Many, {}, false, true, false, false>;
"transfers": $.LinkDesc<$Transfer, $.Cardinality.Many, {}, false, true, false, false>;
"initialization": $.PropertyDesc<$.NamedTupleType<{salt: $Bytes32, bytecodeHash: $Bytes32, aaVersion: $uint16}>, $.Cardinality.One, false, false, false, false>;
"<account[is Proposal]": $.LinkDesc<$Proposal, $.Cardinality.Many, {}, false, false, false, false>;
"<account[is Message]": $.LinkDesc<$Message, $.Cardinality.Many, {}, false, false, false, false>;
"<account[is Transaction]": $.LinkDesc<$Transaction, $.Cardinality.Many, {}, false, false, false, false>;
Expand Down
2 changes: 1 addition & 1 deletion api/dbschema/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ export namespace $default {
"active": boolean;
"implementation": string;
"photo"?: string | null;
"salt": string;
"policies": Policy[];
"approvers": Approver[];
"messages": Message[];
"proposals": Proposal[];
"transactions": Transaction[];
"transfers": Transfer[];
"initialization": {salt: string, bytecodeHash: string, aaVersion: number};
}
export interface Action extends std.$Object {
"functions": ActionFunction[];
Expand Down
16 changes: 16 additions & 0 deletions api/dbschema/migrations/00006-m13y2dt.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE MIGRATION m13y2dtmcrd7yvmorrgn62mp2cakpvrcbz7p7g2rr4wk34b6ow7m7q
ONTO m174x3tft7rqwxzekegp7qad4qlscof7tojfcmw4wielhp7h4mqsya
{
ALTER TYPE default::Account {
CREATE REQUIRED PROPERTY initialization: tuple<salt: default::Bytes32, bytecodeHash: default::Bytes32, aaVersion: default::uint16> {
SET REQUIRED USING ((
salt := .salt,
bytecodeHash := '0x0100007b3eebe76a9052ad76c1efe68151404a98aee77b96cbdbc62df0660b27',
aaVersion := 1
));
};
};
ALTER TYPE default::Account {
DROP PROPERTY salt;
};
};
15 changes: 8 additions & 7 deletions api/src/feat/accounts/accounts.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Test } from '@nestjs/testing';
import { UserContext, asUser, getUserCtx } from '~/core/context';
import { randomLabel, randomAddress, randomUAddress, randomUser } from '~/util/test';
import { getProxyAddress, UAddress } from 'lib';
import { randomAddress, randomLabel, randomUAddress, randomUser } from '~/util/test';
import { UAddress } from 'lib';
import { PoliciesService } from '../policies/policies.service';
import { BullModule, getQueueToken } from '@nestjs/bullmq';
import { ActivationsQueue } from '../activations/activations.queue';
Expand All @@ -12,13 +12,14 @@ import e from '~/edgeql-js';
import { uuid } from 'edgedb/dist/codecs/ifaces';
import { AccountsCacheService } from '../auth/accounts.cache.service';
import { TypedQueue } from '~/core/bull/bull.util';
import { create2Address } from 'zksync-ethers/build/utils';

jest.mock('lib', () => ({
...jest.requireActual('lib'),
getProxyAddress: jest.fn(),
jest.mock('zksync-ethers/build/utils', () => ({
...jest.requireActual('zksync-ethers/build/utils'),
create2Address: jest.fn(),
}));

const getProxyAddressMock = jest.mocked(getProxyAddress);
const create2AddressMock = jest.mocked(create2Address);

describe(AccountsService.name, () => {
let service: AccountsService;
Expand Down Expand Up @@ -51,7 +52,7 @@ describe(AccountsService.name, () => {
const createAccount = async () => {
const userCtx = getUserCtx();

getProxyAddressMock.mockReturnValue(randomAddress());
create2AddressMock.mockReturnValue(randomAddress());

return service.createAccount({
chain: 'zksync-local',
Expand Down
21 changes: 14 additions & 7 deletions api/src/feat/accounts/accounts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import e from '~/edgeql-js';
import {
asPolicyKey,
randomDeploySalt,
getProxyAddress,
Address,
UAddress,
asAddress,
ACCOUNT_IMPLEMENTATION,
asUAddress,
PLACEHOLDER_ACCOUNT_ADDRESS,
ACCOUNT_PROXY,
encodeProxyConstructorArgs,
} from 'lib';
import { CREATE2_FACTORY } from 'lib/dapps';
import { ShapeFunc } from '~/core/database';
Expand All @@ -32,6 +33,8 @@ import { v4 as uuid } from 'uuid';
import { selectAccount2 } from './accounts.util';
import { AccountEvent } from './accounts.model';
import { PolicyInput } from '../policies/policies.input';
import { utils as zkUtils } from 'zksync-ethers';
import { toHex } from 'viem';

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

const implementation = ACCOUNT_IMPLEMENTATION.address[chain];
const bytecodeHash = toHex(zkUtils.hashBytecode(ACCOUNT_PROXY.bytecode));
const address = asUAddress(
getProxyAddress({
deployer: CREATE2_FACTORY.address,
implementation,
zkUtils.create2Address(
CREATE2_FACTORY.address,
bytecodeHash,
salt,
policies: policies.map((p) => inputAsPolicy(p.key, p)),
}),
encodeProxyConstructorArgs({
implementation,
policies: policies.map((p) => inputAsPolicy(p.key, p)),
}),
),
chain,
);

Expand Down Expand Up @@ -147,7 +154,7 @@ export class AccountsService {
address,
name,
implementation,
salt,
initialization: { salt, bytecodeHash, aaVersion: 1 },
}),
);

Expand Down
2 changes: 1 addition & 1 deletion api/src/feat/accounts/insert-account.edgeql
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ insert Account {
address := <UAddress>$address,
name := <str>$name,
implementation := <Address>$implementation,
salt := <Bytes32>$salt
initialization := <tuple<salt: Bytes32, bytecodeHash: Bytes32, aaVersion: uint16>>$initialization
}
8 changes: 6 additions & 2 deletions api/src/feat/accounts/insert-account.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export type InsertAccountArgs = {
readonly "address": string;
readonly "name": string;
readonly "implementation": string;
readonly "salt": string;
readonly "initialization": {
readonly "salt": string;
readonly "bytecodeHash": string;
readonly "aaVersion": number;
};
};

export type InsertAccountReturns = {
Expand All @@ -21,7 +25,7 @@ insert Account {
address := <UAddress>$address,
name := <str>$name,
implementation := <Address>$implementation,
salt := <Bytes32>$salt
initialization := <tuple<salt: Bytes32, bytecodeHash: Bytes32, aaVersion: uint16>>$initialization
}`, args);

}
29 changes: 12 additions & 17 deletions api/src/feat/activations/activations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
replaceSelfAddress,
PLACEHOLDER_ACCOUNT_ADDRESS,
UUID,
ACCOUNT_PROXY,
encodeProxyConstructorArgs,
} from 'lib';
import { CREATE2_FACTORY } from 'lib/dapps';
Expand All @@ -21,13 +20,6 @@ import { FlowJob } from 'bullmq';
import { ConfirmationQueue } from '../system-txs/confirmations.queue';
import Decimal from 'decimal.js';
import { SimulationsQueue } from '../simulations/simulations.worker';
import { toHex } from 'viem';
import { utils as zkUtils } from 'zksync-ethers';

interface FeeParams {
account: UAddress;
feePerGas: Decimal;
}

@Injectable()
export class ActivationsService {
Expand Down Expand Up @@ -61,15 +53,15 @@ export class ActivationsService {
} satisfies FlowJob;
}

async fee({ account, feePerGas }: FeeParams): Promise<Decimal | null> {
const a = await this.db.queryWith(
async fee(account: UAddress): Promise<Decimal | null> {
const a = await this.db.queryWith2(
{ address: e.UAddress },
{ address: account },
({ address }) =>
e.select(e.Account, () => ({
filter_single: { address },
activationEthFee: true,
})),
{ address: account },
);
if (!a) return null;
if (a.activationEthFee) return new Decimal(a.activationEthFee);
Expand All @@ -79,8 +71,11 @@ export class ActivationsService {

const network = this.networks.get(account);
try {
const gas = await network.estimateContractGas(request);
return feePerGas.mul(gas.toString());
const [gas, maxFeePerGas] = await Promise.all([
network.estimateContractGas(request),
network.maxFeePerGas(),
]);
return maxFeePerGas.mul(gas.toString());
} catch (e) {
const isDeployed = !!(await network.getCode({ address: asAddress(account) }))?.length;
if (isDeployed) return null;
Expand All @@ -97,7 +92,7 @@ export class ActivationsService {
filter_single: { address },
active: true,
implementation: true,
salt: true,
initialization: true,
initPolicies: e.select(a.policies, (p) => ({
filter: p.initState,
...PolicyShape,
Expand Down Expand Up @@ -126,10 +121,10 @@ export class ActivationsService {
address: CREATE2_FACTORY.address,
functionName: 'create2Account' as const,
args: [
asHex(account.salt),
toHex(zkUtils.hashBytecode(ACCOUNT_PROXY.bytecode)),
asHex(account.initialization.salt),
asHex(account.initialization.bytecodeHash),
constructorArgs,
1, // AccountAbstractionVersion.Version1
account.initialization.aaVersion,
] as const,
// factoryDeps: [ACCOUNT_PROXY.bytecode], // Throws "rpc method is not whitelisted" if provided; bytecode must be deployed using SystemContractDeployer first
// gas: 3_000_000n * BigInt(params.policies.length), // ~1M per policy; gas estimation panics if not provided
Expand Down
8 changes: 7 additions & 1 deletion api/src/feat/activations/activations.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ActivationsQueue } from './activations.queue';
import { DatabaseService } from '~/core/database';
import e from '~/edgeql-js';
import { selectTransaction2 } from '../transactions/transactions.util';
import { UnrecoverableError } from 'bullmq';
import { asAddress } from 'lib';

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

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

const { account: _, ...req } = request;
const receipt = await network.useWallet((wallet) => wallet.writeContract(req));
Expand Down
4 changes: 1 addition & 3 deletions api/src/feat/paymasters/paymasters.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ export class PaymastersService implements OnModuleInit {
}

async paymasterFees({ account }: PaymasterFeesParams): Promise<PaymasterFeeParts> {
const feePerGas = await this.networks.get(account).maxFeePerGas();

const activation = await this.activations.fee({ account, feePerGas });
const activation = await this.activations.fee(account);

return { activation: activation ?? new Decimal(0) };
}
Expand Down
15 changes: 4 additions & 11 deletions api/src/feat/policies/policies.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { Test } from '@nestjs/testing';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { PoliciesService, ProposePoliciesParams } from './policies.service';
import {
asPolicyKey,
asSelector,
asUUID,
randomDeploySalt,
randomHex,
UAddress,
ZERO_ADDR,
} from 'lib';
import { asPolicyKey, asSelector, asUUID, randomHex, UAddress, ZERO_ADDR } from 'lib';
import { UserContext } from '~/core/context';
import { asUser, getUserCtx } from '~/core/context';
import { randomAddress, randomLabel, randomUAddress, randomUser } from '~/util/test';
Expand All @@ -20,6 +12,7 @@ import { inputAsPolicy, policyStateAsPolicy, PolicyShape, selectPolicy } from '.
import { PolicyInput } from './policies.input';
import { v1 as uuidv1 } from 'uuid';
import { selectAccount } from '../accounts/accounts.util';
import { zeroHash } from 'viem';

describe(PoliciesService.name, () => {
let service: PoliciesService;
Expand Down Expand Up @@ -58,8 +51,8 @@ describe(PoliciesService.name, () => {
id: accountId,
address: account,
name: randomLabel(),
implementation: randomAddress(),
salt: randomDeploySalt(),
implementation: ZERO_ADDR,
initialization: { salt: zeroHash, bytecodeHash: zeroHash, aaVersion: 1 },
}),
);

Expand Down
6 changes: 3 additions & 3 deletions api/src/feat/transactions/transactions.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Test } from '@nestjs/testing';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { asUser, getUserCtx, UserContext } from '~/core/context';
import { DeepPartial, randomAddress, randomLabel, randomUAddress, randomUser } from '~/util/test';
import { randomDeploySalt, Hex, UAddress, ZERO_ADDR, asUUID, asPolicyKey } from 'lib';
import { Hex, UAddress, ZERO_ADDR, asUUID, asPolicyKey } from 'lib';
import { Network, NetworksService } from '~/core/networks/networks.service';
import { ProposeTransactionInput } from './transactions.input';
import { DatabaseService } from '~/core/database';
Expand Down Expand Up @@ -112,8 +112,8 @@ describe(TransactionsService.name, () => {
id: accountId,
address: account,
name: randomLabel(),
implementation: randomAddress(),
salt: randomDeploySalt(),
implementation: ZERO_ADDR,
initialization: { salt: zeroHash, bytecodeHash: zeroHash, aaVersion: 1 },
upgradedAtBlock: 1n,
})
.unlessConflict(),
Expand Down
8 changes: 4 additions & 4 deletions api/src/feat/transfers/transfers.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import {
asChain,
asUAddress,
asUUID,
randomDeploySalt,
} from 'lib';
import { randomAddress, randomLabel, randomUAddress, randomUser } from '~/util/test';
import { randomLabel, randomUAddress, randomUser } from '~/util/test';
import e from '~/edgeql-js';
import { v1 as uuidv1 } from 'uuid';
import { InsertShape } from '~/edgeql-js/insert';
import { $Transfer } from '~/edgeql-js/modules/default';
import { zeroHash } from 'viem';

describe(TransfersService.name, () => {
let service: TransfersService;
Expand All @@ -45,8 +45,8 @@ describe(TransfersService.name, () => {
id,
address,
name: randomLabel(),
implementation: randomAddress(),
salt: randomDeploySalt(),
implementation: ZERO_ADDR,
initialization: { salt: zeroHash, bytecodeHash: zeroHash, aaVersion: 1 },
upgradedAtBlock: 1n,
})
.unlessConflict()
Expand Down
2 changes: 1 addition & 1 deletion contracts/script/deployProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TASK_COMPILE } from 'hardhat/builtin-tasks/task-names';
import AccountProxy from './contracts/AccountProxy';
import { deploy } from './util/deploy';

const ACCOUNT = '0xc0c1c0692F1aCd4FA91ccc73fB9aCFCd60Dd571a';
const ACCOUNT = '0x696532D64a358a4CC2eCDBE698a4a08c7841af8c';

// Deploys an uninitialized proxy
// Used for first-time proxy bytecode deployment
Expand Down
Loading

0 comments on commit 8f6ed00

Please sign in to comment.