Skip to content

Commit cb6863c

Browse files
authored
chore: backport DeployMethod refactor (#22985) to v4-next (#23029)
## Summary Backport of [#22985](#22985) (`fix: better DeployMethod`) to `backport-to-v4-next-staging`. The original PR refactors `DeployMethod` so that all address-affecting parameters (`salt`, `deployer`, `universalDeploy`, `publicKeys`) are locked in at construction time via a new `DeployInstantiationOptions` argument, and removes the silent salt-cache poisoning bug where the address could change between calls. See the migration notes added under `## TBD` for the full API change. ## Commits This backport preserves the standard 3-commit history so reviewers can see exactly what conflicted and how it was resolved: 1. **`chore: cherry-pick #22985 ... with conflicts`** — raw `git cherry-pick` result with conflict markers in place. Does not compile. 2. **`fix: resolve cherry-pick conflicts`** — resolution. Notable choices: - `migration_notes.md`: only the new DeployMethod note was added. Other entries that came in via the cherry-pick (`getBlock` / `getCheckpoint`, `feeAssetPriceModifier`, Domain separators, aztec-up table reformatting) belong to PRs that have not been backported, so they were dropped to avoid bringing in unrelated changes. - `txe_oracles.nr`, `constants.nr`, `constants_tests.nr`: dropped formatting-only diffs that depended on constants/types not present on v4-next (`MAX_PRIVATE_LOGS_PER_TX`, `PRIVATE_LOG_SIZE_IN_FIELDS`, `DOM_SEP__HANDSHAKE_SECRET_HASH`, `DOM_SEP__MERKLE_HASH`, etc.). - `bot/src/factory.ts`: dropped the new `setupTokenWithOptionalEarlyRefuel` / `setupTokenContractWithOptionalEarlyRefuel` / `getTokenInstance` helpers (introduced by an unrelated PR) and applied the new `DeployInstantiationOptions` API to the existing `setupToken` flow. - `e2e/composed/ha/e2e_ha_full.test.ts`: dropped the `should reload keystore via admin API and keep building blocks after swapping attesters` test that does not exist on v4-next. - `docs/examples/ts/aave_bridge`, `docs/examples/ts/example_swap`: kept deleted (modify/delete conflict — these examples don't exist on v4-next). - `docs/examples/ts/token_bridge/index.ts`: kept `node.getProvenBlockNumber()` — the new `getBlockNumber('proven')` is from a different PR. - End-to-end tests, `aztec.js/src/api/contract.ts`, `bot`, etc.: adopted the new `DeployInstantiationOptions` API; kept v4-next-only `DeployTxReceipt` / `DeployWaitOptions` exports. 3. **`fix: restore DeployInteractionWaitOptions for v4-next returnReceipt API`** — v4-next still supports `wait: { returnReceipt: true }` (a feature added on v4-next that does not exist upstream). The upstream PR removed `DeployInteractionWaitOptions` and tightened `DeployOptions<W extends InteractionWaitOptions>`, which broke type-checking for the existing `returnReceipt` callers. This commit restores `DeployInteractionWaitOptions = NoWait | DeployWaitOptions | undefined` and rewires `DeployOptions` / `DeployReturn` / `send` to use it, so the v4-next-only `returnReceipt: true` callers keep type-checking. ## Test plan - [ ] CI green on this branch - [ ] e2e_deploy_contract tests pass ClaudeBox log: https://claudebox.work/s/2e03d384f1f73fd0?run=1
2 parents a53a37b + bd7471f commit cb6863c

38 files changed

Lines changed: 847 additions & 612 deletions

File tree

boxes/boxes/react/src/hooks/useContract.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@ export function useContract() {
1515
setWait(true);
1616
const wallet = await deployerEnv.getWallet();
1717
const defaultAccountAddress = deployerEnv.getDefaultAccountAddress();
18-
const salt = Fr.random();
1918

2019
const { BoxReactContract } = await import('../../artifacts/BoxReact');
2120

2221
const deploymentPromise = BoxReactContract.deploy(wallet, Fr.random(), defaultAccountAddress).send({
2322
from: defaultAccountAddress,
24-
contractAddressSalt: salt,
2523
});
2624

2725
const { contract } = await toast.promise(deploymentPromise, {

boxes/boxes/vanilla/scripts/deploy.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,10 @@ async function deployContract(wallet: Wallet, deployer: AztecAddress) {
7272

7373
const sponsoredPFCContract = await getSponsoredPFCContract();
7474

75-
const { contract } = await PrivateVotingContract.deploy(
76-
wallet,
77-
deployer
78-
).send({
75+
const { contract } = await PrivateVotingContract.deploy(wallet, deployer, {
76+
salt,
77+
}).send({
7978
from: deployer,
80-
contractAddressSalt: salt,
8179
fee: {
8280
paymentMethod: new SponsoredFeePaymentMethod(
8381
sponsoredPFCContract.address

boxes/boxes/vite/src/hooks/useContract.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@ export function useContract() {
1515
setWait(true);
1616
const wallet = await deployerEnv.getWallet();
1717
const defaultAccountAddress = deployerEnv.getDefaultAccountAddress();
18-
const salt = Fr.random();
1918

2019
const { BoxReactContract } = await import('../../artifacts/BoxReact');
2120

2221
const deploymentPromise = BoxReactContract.deploy(wallet, Fr.random(), defaultAccountAddress).send({
2322
from: defaultAccountAddress,
24-
contractAddressSalt: salt,
2523
});
2624

2725
const { contract } = await toast.promise(deploymentPromise, {

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,97 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12+
### [Aztec.js] `DeployMethod` address-affecting parameters move to construction time
13+
14+
Salt, deployer, and public keys are now passed when the `DeployMethod` is constructed, not on every call to `send` / `simulate` / `request` / `getInstance`. This locks the contract address once it is determined and prevents the silent salt-cache poisoning bug where the address could change between calls.
15+
16+
`contractAddressSalt`, `deployer`, and `universalDeploy` have been removed from `DeployOptions`, `RequestDeployOptions`, and `SimulateDeployOptions`. They now live on a new `DeployInstantiationOptions` argument passed at construction. `deployer` and `universalDeploy` are mutually exclusive; passing both throws. `Contract.deployWithPublicKeys` and the generated `MyContract.deployWithPublicKeys(...)` factories have been removed; pass `publicKeys` via the `instantiation` argument of `deploy(...)` instead. The buggy synchronous `address` and `partialAddress` getters have been removed and replaced with `getAddress()` and `getPartialAddress()` (both `async`).
17+
18+
The compact form keeps working: `MyContract.deploy(wallet, ...args).send({ from: alice })` deploys with `deployer = alice` and `salt = random()`, exactly as before. The deployer is locked the first time `send` / `simulate` / `profile` is called (from `options.from`, with `NO_FROM` or undefined → universal) and cannot change after that:
19+
20+
- Subsequent `send` / `simulate` / `profile` calls with a `from` that would imply a different deployer throw, instead of silently producing a different address.
21+
- A lock to universal (`AztecAddress.ZERO`) is the only one compatible with any sender, since the universal address does not depend on `from`.
22+
- A lock to a concrete address only accepts that exact `from` on subsequent calls.
23+
24+
**Migration:**
25+
26+
Universal deployment with a fixed salt:
27+
28+
```diff
29+
- const deploy = MyContract.deploy(wallet, ...args);
30+
- await deploy.send({
31+
- from: alice,
32+
- contractAddressSalt: salt,
33+
- universalDeploy: true,
34+
- });
35+
+ const deploy = MyContract.deploy(wallet, ...args, { salt, universalDeploy: true });
36+
+ await deploy.send({ from: alice });
37+
```
38+
39+
Non-universal deploy where `from` doubles as the deployer:
40+
41+
```diff
42+
- const deploy = MyContract.deploy(wallet, ...args);
43+
- await deploy.send({ from: alice, contractAddressSalt: salt });
44+
+ const deploy = MyContract.deploy(wallet, ...args, { salt });
45+
+ await deploy.send({ from: alice });
46+
```
47+
48+
If you need to read the address before sending, lock the deployer at construction:
49+
50+
```typescript
51+
const deploy = MyContract.deploy(wallet, ...args, { salt, deployer: alice });
52+
const address = await deploy.getAddress(); // resolves; deployer was locked at construction
53+
await deploy.send({ from: alice }); // deploys at the address `getAddress` returned
54+
```
55+
56+
Universal deploys can be sent by any account, since the universal address does not depend on `from`:
57+
58+
```typescript
59+
const deploy = MyContract.deploy(wallet, ...args, { universalDeploy: true });
60+
await deploy.send({ from: bob }); // OK, universal accepts any sender
61+
```
62+
63+
A lock to a concrete deployer rejects sending from a different account, instead of silently deploying at a different address:
64+
65+
```typescript
66+
const deploy = MyContract.deploy(wallet, ...args, { deployer: alice });
67+
await deploy.send({ from: bob }); // throws: deployer is locked to alice
68+
```
69+
70+
`deployWithPublicKeys` is gone; pass `publicKeys` in the instantiation options instead:
71+
72+
```diff
73+
- const deploy = MyContract.deployWithPublicKeys(publicKeys, wallet, ...args);
74+
+ const deploy = MyContract.deploy(wallet, ...args, { publicKeys });
75+
```
76+
77+
`ContractDeployer.deploy(...)` now takes the instantiation argument as its first parameter (pass `{}` to use defaults and rely on lazy locking from `from`):
78+
79+
```diff
80+
- const cd = new ContractDeployer(artifact, wallet);
81+
- await cd.deploy(...ctorArgs).send({ from: alice, contractAddressSalt: salt });
82+
+ const cd = new ContractDeployer(artifact, wallet);
83+
+ await cd.deploy(ctorArgs, { salt }).send({ from: alice });
84+
```
85+
86+
The synchronous `address` / `partialAddress` getters are gone:
87+
88+
```diff
89+
- const address = deploy.address; // sync, possibly undefined
90+
- const partial = await deploy.partialAddress; // sync getter wrapping async value
91+
+ const address = await deploy.getAddress(); // requires the deployer to be locked
92+
+ const partial = await deploy.getPartialAddress(); // requires the deployer to be locked
93+
```
94+
95+
`getInstance()` no longer takes options; use the construction-time instantiation instead:
96+
97+
```diff
98+
- const instance = await deploy.getInstance({ contractAddressSalt: salt });
99+
+ const deploy = MyContract.deploy(wallet, ...args, { salt, deployer: alice });
100+
+ const instance = await deploy.getInstance();
101+
```
102+
12103
### [Aztec.nr] TXE `call_public_incognito` no longer takes a `from` parameter
13104

14105
`TestEnvironment::call_public_incognito` previously accepted a `from` address that was silently ignored (the function always uses a null `msg_sender`). The `from` parameter has been removed.

docs/examples/ts/aztecjs_advanced/index.ts

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@ import { getPublicEvents } from "@aztec/aztec.js/events";
1616
import { GasSettings } from "@aztec/stdlib/gas";
1717

1818
// Setup: connect to network
19-
const node = createAztecNodeClient(process.env.AZTEC_NODE_URL ?? "http://localhost:8080");
19+
const node = createAztecNodeClient(
20+
process.env.AZTEC_NODE_URL ?? "http://localhost:8080",
21+
);
2022
await waitForNode(node);
2123
const wallet = await EmbeddedWallet.create(node, { ephemeral: true });
2224

2325
const testAccounts = await getInitialTestAccountsData();
2426
const [aliceAddress, bobAddress] = await Promise.all(
2527
testAccounts.slice(0, 2).map(async (account) => {
26-
return (await wallet.createSchnorrAccount(account.secret, account.salt, account.signingKey)).address;
28+
return (
29+
await wallet.createSchnorrAccount(
30+
account.secret,
31+
account.salt,
32+
account.signingKey,
33+
)
34+
).address;
2735
}),
2836
);
2937

@@ -45,8 +53,13 @@ const sponsoredFPCInstance = await getContractInstanceFromInstantiationParams(
4553
SponsoredFPCContract.artifact,
4654
{ salt: new Fr(0) },
4755
);
48-
await wallet.registerContract(sponsoredFPCInstance, SponsoredFPCContract.artifact);
49-
const sponsoredPaymentMethod = new SponsoredFeePaymentMethod(sponsoredFPCInstance.address);
56+
await wallet.registerContract(
57+
sponsoredFPCInstance,
58+
SponsoredFPCContract.artifact,
59+
);
60+
const sponsoredPaymentMethod = new SponsoredFeePaymentMethod(
61+
sponsoredFPCInstance.address,
62+
);
5063

5164
// wallet is from the connection guide; sponsoredPaymentMethod is from the fees guide
5265
const { contract: sponsoredContract } = await TokenContract.deploy(
@@ -68,9 +81,9 @@ const { contract: saltedContract } = await TokenContract.deploy(
6881
"SaltedToken",
6982
"SALT",
7083
18,
84+
{ salt: customSalt },
7185
).send({
7286
from: aliceAddress,
73-
contractAddressSalt: customSalt,
7487
});
7588
// docs:end:deploy_custom_salt
7689

@@ -84,8 +97,9 @@ const deployMethod = TokenContract.deploy(
8497
"PredictedToken",
8598
"PRED",
8699
18,
100+
{ salt: deploymentSalt, deployer: aliceAddress },
87101
);
88-
const instance = await deployMethod.getInstance({ contractAddressSalt: deploymentSalt });
102+
const instance = await deployMethod.getInstance();
89103
const predictedAddress = instance.address;
90104

91105
console.log(`Contract will deploy at: ${predictedAddress}`);
@@ -235,15 +249,21 @@ console.log(`Derived token at: ${derivedToken.address.toString()}`);
235249
// docs:start:parallel_deploy
236250
// Deploy contracts in parallel using Promise.all
237251
const contracts = await Promise.all([
238-
TokenContract.deploy(wallet, aliceAddress, "Token1", "T1", 18).send({
239-
from: aliceAddress,
240-
}).then(({ contract }) => contract),
241-
TokenContract.deploy(wallet, aliceAddress, "Token2", "T2", 18).send({
242-
from: aliceAddress,
243-
}).then(({ contract }) => contract),
244-
TokenContract.deploy(wallet, aliceAddress, "Token3", "T3", 18).send({
245-
from: aliceAddress,
246-
}).then(({ contract }) => contract),
252+
TokenContract.deploy(wallet, aliceAddress, "Token1", "T1", 18)
253+
.send({
254+
from: aliceAddress,
255+
})
256+
.then(({ contract }) => contract),
257+
TokenContract.deploy(wallet, aliceAddress, "Token2", "T2", 18)
258+
.send({
259+
from: aliceAddress,
260+
})
261+
.then(({ contract }) => contract),
262+
TokenContract.deploy(wallet, aliceAddress, "Token3", "T3", 18)
263+
.send({
264+
from: aliceAddress,
265+
})
266+
.then(({ contract }) => contract),
247267
]);
248268

249269
console.log(`Contract 1 at: ${contracts[0].address}`);
@@ -293,8 +313,12 @@ async function pollForTransferEvents() {
293313

294314
for (const { event, metadata } of events) {
295315
// Process each transfer event
296-
console.log(`Transfer: ${event.amount} from ${event.from} to ${event.to}`);
297-
console.log(` in block ${metadata.l2BlockNumber}, tx ${metadata.txHash}`);
316+
console.log(
317+
`Transfer: ${event.amount} from ${event.from} to ${event.to}`,
318+
);
319+
console.log(
320+
` in block ${metadata.l2BlockNumber}, tx ${metadata.txHash}`,
321+
);
298322
}
299323

300324
lastProcessedBlock = currentBlock;
@@ -307,7 +331,11 @@ await pollForTransferEvents();
307331

308332
// docs:start:connect_to_contract
309333
// wallet is from the connection guide; token is the contract deployed in the deploy guide
310-
const contract = await Contract.at(token.address, TokenContract.artifact, wallet);
334+
const contract = await Contract.at(
335+
token.address,
336+
TokenContract.artifact,
337+
wallet,
338+
);
311339
// docs:end:connect_to_contract
312340

313341
// docs:start:basic_send_transaction
@@ -333,7 +361,10 @@ console.log("DA gas limit:", metaResult.estimatedGas.gasLimits.daGas);
333361
// docs:end:simulate_with_metadata
334362

335363
// docs:start:read_public_logs
336-
const publicLogs = await node.getPublicLogs({ fromBlock: 1, toBlock: await node.getBlockNumber() + 1 });
364+
const publicLogs = await node.getPublicLogs({
365+
fromBlock: 1,
366+
toBlock: (await node.getBlockNumber()) + 1,
367+
});
337368
if (publicLogs.logs.length > 0) {
338369
const rawFields = publicLogs.logs[0].log.getEmittedFields(); // Fr[]
339370
console.log("Raw log fields:", rawFields.length);
@@ -385,24 +416,32 @@ const networkFees = await node.getCurrentMinFees();
385416
const gasSettings = GasSettings.from({
386417
gasLimits: { daGas: 100_000, l2Gas: 2_000_000 },
387418
teardownGasLimits: { daGas: 100_000, l2Gas: 2_000_000 },
388-
maxFeesPerGas: { feePerDaGas: networkFees.feePerDaGas * 2n, feePerL2Gas: networkFees.feePerL2Gas * 2n },
419+
maxFeesPerGas: {
420+
feePerDaGas: networkFees.feePerDaGas * 2n,
421+
feePerL2Gas: networkFees.feePerL2Gas * 2n,
422+
},
389423
maxPriorityFeesPerGas: { feePerDaGas: 0n, feePerL2Gas: 0n },
390424
});
391425
// docs:end:custom_gas_settings
392426

393427
// docs:start:send_with_gas_settings
394-
const { receipt: gsReceipt } = await token.methods.mint_to_public(aliceAddress, 1n).send({
395-
from: aliceAddress,
396-
fee: { gasSettings },
397-
});
428+
const { receipt: gsReceipt } = await token.methods
429+
.mint_to_public(aliceAddress, 1n)
430+
.send({
431+
from: aliceAddress,
432+
fee: { gasSettings },
433+
});
398434
// docs:end:send_with_gas_settings
399435

400436
// docs:start:read_logs_by_filter
401437
// Get logs for a specific transaction
402438
const txLogs = await node.getPublicLogs({ txHash: gsReceipt.txHash });
403439

404440
// Get logs for a block range
405-
const rangeLogs = await node.getPublicLogs({ fromBlock: 1, toBlock: await node.getBlockNumber() + 1 });
441+
const rangeLogs = await node.getPublicLogs({
442+
fromBlock: 1,
443+
toBlock: (await node.getBlockNumber()) + 1,
444+
});
406445
// docs:end:read_logs_by_filter
407446

408447
// docs:start:auto_gas_estimation
@@ -435,5 +474,4 @@ const { result: privateBalance } = await token.methods
435474
.simulate({ from: aliceAddress });
436475
// docs:end:simulate_private_access
437476

438-
439477
console.log("All advanced examples completed successfully");

docs/examples/ts/token_bridge/index.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ console.log(`NFTPortal: ${portalAddress}\n`);
6363
// docs:start:deploy_l2_contracts
6464
console.log("Deploying L2 contracts...\n");
6565

66-
const { contract: l2Nft } = await NFTPunkContract.deploy(aztecWallet, account.address).send({
66+
const { contract: l2Nft } = await NFTPunkContract.deploy(
67+
aztecWallet,
68+
account.address,
69+
).send({
6770
from: account.address,
6871
});
6972

@@ -201,11 +204,9 @@ async function mine2Blocks(
201204
) {
202205
await NFTPunkContract.deploy(aztecWallet, accountAddress).send({
203206
from: accountAddress,
204-
contractAddressSalt: Fr.random(),
205207
});
206208
await NFTPunkContract.deploy(aztecWallet, accountAddress).send({
207209
from: accountAddress,
208-
contractAddressSalt: Fr.random(),
209210
});
210211
}
211212
// docs:end:mine_blocks
@@ -303,7 +304,11 @@ while (provenBlockNumber < exitReceipt.blockNumber!) {
303304
console.log("Block proven!\n");
304305

305306
// Compute the membership witness using the message hash and the L2 tx hash
306-
const witness = await computeL2ToL1MembershipWitness(node, msgLeaf, exitReceipt.txHash);
307+
const witness = await computeL2ToL1MembershipWitness(
308+
node,
309+
msgLeaf,
310+
exitReceipt.txHash,
311+
);
307312
const epoch = witness!.epochNumber;
308313
console.log(` Epoch for block ${exitReceipt.blockNumber}: ${epoch}`);
309314

playground/src/components/contract/components/CreateContractDialog.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,13 @@ export function CreateContractDialog({
115115
if (publiclyDeploy) {
116116
const postDeployCtor = (instance: ContractInstanceWithAddress, wallet: Wallet) =>
117117
Contract.at(instance.address, contractArtifact, wallet);
118-
deployMethod = new DeployMethod(
119-
contract.publicKeys,
120-
wallet,
121-
contractArtifact,
122-
postDeployCtor,
123-
parameters,
124-
initializer?.name,
125-
);
118+
deployMethod = new DeployMethod(wallet, contractArtifact, postDeployCtor, parameters, initializer?.name, {
119+
publicKeys: contract.publicKeys,
120+
salt: contract.salt,
121+
deployer: from,
122+
});
126123
opts = {
127124
from,
128-
contractAddressSalt: salt,
129125
fee: { paymentMethod: feePaymentMethod },
130126
};
131127
onClose(contract, publiclyDeploy, deployMethod, opts);

yarn-project/aztec.js/src/api/contract.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export { DefaultWaitOpts, type WaitOpts } from '../contract/wait_opts.js';
7171
export { ContractBase, type ContractMethod, type ContractStorageLayout } from '../contract/contract_base.js';
7272
export { BatchCall } from '../contract/batch_call.js';
7373
export {
74+
type DeployInstantiationOptions,
7475
type DeployOptions,
7576
type DeployResultMined,
7677
type DeployReturn,

0 commit comments

Comments
 (0)