Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6bc5656
feat: compute shares address inside vault constructor
ilpepepig Mar 6, 2026
eecf15a
feat: keep constructors public
ilpepepig Mar 6, 2026
577cdfe
test: skip noir test until custom salt deployments are available in TXE
ilpepepig Mar 11, 2026
0841b14
test: test: fix noir test utils and update TXE TODO
zkfrov Mar 13, 2026
abd997c
Merge branch 'dev' into feat/1-step-vault-initialization
ilpepepig Mar 19, 2026
fcb4426
fix: reintroduce comments & remove `to` abstraction in constructor
ilpepepig Mar 19, 2026
f64d3d3
test: use correct deployment flow of shares and vault
ilpepepig Mar 19, 2026
a913d5b
fix: format
ilpepepig Mar 19, 2026
79a498d
Merge branch 'dev' into feat/1-step-vault-initialization
ilpepepig Mar 19, 2026
c71bf3b
feat: simplify token address derivation function
ilpepepig Mar 20, 2026
305bd66
Merge branch 'dev' into feat/1-step-vault-initialization
ilpepepig Mar 20, 2026
15ec72a
fix: js test vault deployments
ilpepepig Mar 20, 2026
0d57b53
test: add class id computation for token contract instead of dummy id
ilpepepig Mar 20, 2026
3e1ca5e
test: assert token and vault addresses deployed and precomputed
ilpepepig Mar 20, 2026
dfb18de
Merge branch 'dev' into feat/1-step-vault-initialization
ilpepepig Mar 23, 2026
543d0b3
Merge branch 'dev' into feat/1-step-vault-initialization
ilpepepig Mar 31, 2026
55f9d47
test: reenable salt dependent vault noir tests
ilpepepig Mar 31, 2026
9498a5c
style: nargo fmt
ilpepepig Mar 31, 2026
7c1dbb0
Merge branch 'dev' into feat/1-step-vault-initialization
ilpepepig Apr 2, 2026
7aa4c9f
Merge branch 'dev' into feat/1-step-vault-initialization
ilpepepig Apr 6, 2026
a1c2e20
docs: remove outdated TODO
ilpepepig Apr 6, 2026
a68d1ea
test: fix truncated error message
ilpepepig Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 86 additions & 45 deletions src/ts/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ interface WalletWithInternals {
pxe: { proveTx(txRequest: TxExecutionRequest, scopes: AztecAddress[]): Promise<TxProvingResult> };
}

import { TokenContract } from '../../../src/artifacts/Token.js';
import { TokenContract, TokenContractArtifact } from '../../../src/artifacts/Token.js';
import { VaultContract, VaultContractArtifact } from '../../../src/artifacts/Vault.js';
import { NFTContract } from '../../../src/artifacts/NFT.js';
import { TestLogicContract } from '../../../src/artifacts/TestLogic.js';
Expand Down Expand Up @@ -241,9 +241,9 @@ export async function deployNFTWithMinter(wallet: EmbeddedWallet, deployer: Azte

/**
* Deploys 3 contracts: asset token, shares token, and vault.
* The vault is deployed first without initializer to get its address,
* then the shares token is deployed with the vault as minter,
* then the vault is initialized with asset and shares addresses.
* The vault constructor computes the shares token address deterministically from the token
* class ID, salt, deployer, and constructor args. The shares token must be deployed with the
* same salt and deployer so the computed address matches.
* @param wallet - The wallet to deploy the contract with.
* @param deployer - The account to deploy the contract with.
* @returns [vault, asset, shares] contract instances.
Expand All @@ -262,14 +262,20 @@ export async function deployVaultAndAssetWithMinter(
deployer,
).send({ ...options, from: deployer });

// Precompute vault address (no shares needed — breaks circular dependency)
const salt = Fr.random();
// Get token class ID for shares address derivation
const tokenClass = await getContractClassFromArtifact(TokenContractArtifact);
const sharesClassId = tokenClass.id;

const vaultSalt = Fr.random();
const sharesSalt = Fr.random();

// Precompute vault address (constructor computes shares address internally)
const { address: vaultAddress } = await deriveContractAddressWithConstructor(
VaultContractArtifact,
'constructor',
[deployer, assetContract.address, 1],
[assetContract.address, 1, 'SharesToken', 'ST', 18, sharesClassId, sharesSalt],
deployer,
salt,
vaultSalt,
);

// Deploy shares token with precomputed vault address as minter
Expand All @@ -279,25 +285,32 @@ export async function deployVaultAndAssetWithMinter(
'ST',
18,
vaultAddress,
).send({ ...options, from: deployer });
).send({ ...options, from: deployer, contractAddressSalt: sharesSalt });

// Deploy vault at precomputed address with #[initializer] constructor
const { contract: vaultContract } = await VaultContract.deploy(wallet, deployer, assetContract.address, 1).send({
...options,
from: deployer,
contractAddressSalt: salt,
});
// Deploy vault — constructor takes shares salt as param and reads deployer from AVM
const { contract: vaultContract } = await VaultContract.deployWithOpts(
{ method: 'constructor', wallet },
assetContract.address,
1,
'SharesToken',
'ST',
18,
sharesClassId,
sharesSalt,
).send({ ...options, from: deployer, contractAddressSalt: vaultSalt });

// Set the shares token on the vault (admin-only, one-shot)
await vaultContract.methods.set_shares_token(sharesContract.address).send({ from: deployer });
expect(vaultContract.address.equals(vaultAddress)).toBe(true);
const { result: onChainSharesAddress } = await vaultContract.methods.shares().simulate({ from: deployer });
expect(onChainSharesAddress.equals(sharesContract.address)).toBe(true);

return [vaultContract as VaultContract, assetContract as TokenContract, sharesContract as TokenContract];
}

/**
* Deploys a vault with an optional initial deposit for inflation-attack protection.
* Deploys 3 contracts: vault (without initializer), shares token (with vault as minter), then initializes vault.
* If initialDeposit > 0, authorizes vault to transfer assets and deposits them.
* If initialDeposit > 0, uses constructor_with_initial_deposit which transfers assets from
* the depositor and mints permanently-locked shares to the vault during initialization.
* Otherwise, uses the regular constructor.
* @returns [vault, shares] contract instances.
*/
export async function deployVaultWithInitialDeposit(
Expand All @@ -308,14 +321,25 @@ export async function deployVaultWithInitialDeposit(
depositor: AztecAddress,
options?: DeployOptions,
): Promise<[VaultContract, TokenContract]> {
// Precompute vault address (no shares needed — breaks circular dependency)
const salt = Fr.random();
const tokenClass = await getContractClassFromArtifact(TokenContractArtifact);
const sharesClassId = tokenClass.id;

const vaultSalt = Fr.random();
const sharesSalt = Fr.random();
const useInitialDeposit = initialDeposit > 0n;

// Constructor choice determines the address
const constructorName = useInitialDeposit ? 'constructor_with_initial_deposit' : 'constructor';
const constructorArgs = useInitialDeposit
? [assetContract.address, 1, 'SharesToken', 'ST', 18, sharesClassId, sharesSalt, initialDeposit, depositor, 0]
: [assetContract.address, 1, 'SharesToken', 'ST', 18, sharesClassId, sharesSalt];

const { address: vaultAddress } = await deriveContractAddressWithConstructor(
VaultContractArtifact,
'constructor',
[deployer, assetContract.address, 1],
constructorName,
constructorArgs,
deployer,
salt,
vaultSalt,
);

// Deploy shares token with precomputed vault address as minter
Expand All @@ -325,35 +349,52 @@ export async function deployVaultWithInitialDeposit(
'ST',
18,
vaultAddress,
).send({ ...options, from: deployer });

// Deploy vault at precomputed address with #[initializer] constructor
const { contract: vaultContract } = await VaultContract.deploy(wallet, deployer, assetContract.address, 1).send({
...options,
from: deployer,
contractAddressSalt: salt,
});
).send({ ...options, from: deployer, contractAddressSalt: sharesSalt });

if (initialDeposit > 0n) {
// Authorize vault to transfer assets from depositor
const transfer = assetContract.methods.transfer_public_to_public(
depositor,
vaultContract.address,
const transfer = assetContract.methods.transfer_public_to_public(depositor, vaultAddress, initialDeposit, 0);
await setPublicAuthWit(vaultAddress, transfer, depositor, wallet as EmbeddedWallet);

// Deploy vault with initial deposit
const { contract: vaultContract } = await VaultContract.deployWithOpts(
{ method: 'constructor_with_initial_deposit', wallet },
assetContract.address,
1,
'SharesToken',
'ST',
18,
sharesClassId,
sharesSalt,
initialDeposit,
depositor,
0,
);
await setPublicAuthWit(vaultContract.address, transfer, depositor, wallet as EmbeddedWallet);
).send({
...options,
from: deployer,
contractAddressSalt: vaultSalt,
});

// Set shares and make initial deposit
await vaultContract.methods
.set_shares_token_with_initial_deposit(sharesContract.address, initialDeposit, depositor, 0)
.send({ from: deployer });
return [vaultContract as VaultContract, sharesContract as TokenContract];
} else {
// Just set shares without initial deposit
await vaultContract.methods.set_shares_token(sharesContract.address).send({ from: deployer });
}
// Deploy vault at precomputed address
const { contract: vaultContract } = await VaultContract.deployWithOpts(
{ method: 'constructor', wallet },
assetContract.address,
1,
'SharesToken',
'ST',
18,
sharesClassId,
sharesSalt,
).send({
...options,
from: deployer,
contractAddressSalt: vaultSalt,
});

return [vaultContract as VaultContract, sharesContract as TokenContract];
return [vaultContract as VaultContract, sharesContract as TokenContract];
}
}

// --- Escrow Utils ---
Expand Down
Loading
Loading