Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

Commit a3859e5

Browse files
feat: ARC-1155 (#351)
# 🤖 Linear Closes AZT-853 ## Description Multitoken contract --------- Signed-off-by: ilpepepig <167773062+ilpepepig@users.noreply.github.com> Signed-off-by: 0xLeopoldo <leopoldo@wonderland.xyz> Co-authored-by: ilpepepig <167773062+ilpepepig@users.noreply.github.com> Co-authored-by: ilpepepig <ilpepepig@defi.sucks>
1 parent 3ac69e7 commit a3859e5

30 files changed

Lines changed: 4291 additions & 1 deletion

Nargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ members = [
66
"src/vault_deployer",
77
"src/dripper",
88
"src/nft_contract",
9+
"src/multitoken_contract",
10+
"src/multitoken_contract/src/test/multitoken_authorization_contract",
911
"src/escrow_contract",
1012
"src/escrow_contract/src/test/test_logic_contract",
1113
"src/generic_proxy",
@@ -14,6 +16,7 @@ members = [
1416
[benchmark]
1517
token = "benchmarks/token_contract.benchmark.ts"
1618
nft = "benchmarks/nft_contract.benchmark.ts"
19+
multitoken = "benchmarks/multitoken_contract.benchmark.ts"
1720
vault = "benchmarks/vault_contract.benchmark.ts"
1821
escrow = "benchmarks/escrow_contract.benchmark.ts"
1922
logic = "benchmarks/logic_contract.benchmark.ts"
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { Wallet } from '@aztec/aztec.js/wallet';
2+
import { AztecAddress } from '@aztec/aztec.js/addresses';
3+
import type { ContractFunctionInteractionCallIntent } from '@aztec/aztec.js/authorization';
4+
5+
// Import the new Benchmark base class and context
6+
import { Benchmark, BenchmarkContext } from '@defi-wonderland/aztec-benchmark';
7+
8+
import { MultiTokenContract } from '../src/artifacts/MultiToken.js';
9+
import {
10+
deployMultiTokenWithMinter,
11+
initializeMultiTokenTransferCommitment,
12+
setupTestSuite,
13+
ID_A,
14+
} from '../src/ts/test/utils.js';
15+
16+
// Extend the BenchmarkContext from the new package
17+
interface MultiTokenBenchmarkContext extends BenchmarkContext {
18+
cleanup: () => Promise<void>;
19+
wallet: Wallet;
20+
deployer: AztecAddress;
21+
accounts: AztecAddress[];
22+
multiTokenContract: MultiTokenContract;
23+
commitments: bigint[];
24+
}
25+
26+
// --- Helper Functions ---
27+
28+
function amt(x: bigint | number) {
29+
// MultiToken carries NO decimals (its constructor takes only name/symbol/minter/auth_contract, unlike the
30+
// Token contract's `decimals` arg), so amounts are raw u128 values. We keep the sibling token benchmark's
31+
// magnitudes (mint 100, move 10) without the 10**18 scaling that `parseUnits` would apply.
32+
return BigInt(x);
33+
}
34+
35+
// Use export default class extending Benchmark
36+
export default class MultiTokenContractBenchmark extends Benchmark {
37+
/**
38+
* Sets up the benchmark environment for the MultiTokenContract.
39+
* Creates wallet, gets accounts, and deploys the contract.
40+
*/
41+
async setup(): Promise<MultiTokenBenchmarkContext> {
42+
const { cleanup, wallet, accounts } = await setupTestSuite(true);
43+
const [deployer] = accounts;
44+
// minter = deployer, auth_contract = ZERO (ARC-403 hook disabled) — mirrors the default JS suite deploy.
45+
const multiTokenContract = await deployMultiTokenWithMinter(wallet, deployer, deployer, AztecAddress.ZERO);
46+
47+
// Pre-initialize the partial notes consumed by transfer_private_to_commitment / transfer_public_to_commitment.
48+
// The commitment is id-agnostic (the completer binds the id at completion); the payer (alice) must be the
49+
// completer, and the note must come from a PRIOR settled tx (the helper settles each one).
50+
const [alice, bob] = accounts;
51+
const commitment_1 = await initializeMultiTokenTransferCommitment(multiTokenContract, alice, bob, alice);
52+
const commitment_2 = await initializeMultiTokenTransferCommitment(multiTokenContract, alice, bob, alice);
53+
54+
const commitments = [commitment_1, commitment_2];
55+
56+
return { cleanup, wallet, deployer, accounts, multiTokenContract, commitments };
57+
}
58+
59+
/**
60+
* Returns the list of MultiTokenContract methods to be benchmarked.
61+
* Ordering matters: the mints seed the balances that the following transfers/burns/commitments spend.
62+
* Every op is an `alice` self-spend of token id `ID_A` (nonce = 0, so no authwit is needed).
63+
*/
64+
getMethods(context: MultiTokenBenchmarkContext): ContractFunctionInteractionCallIntent[] {
65+
const { multiTokenContract, accounts, wallet, commitments } = context;
66+
const [alice, bob] = accounts;
67+
const owner = alice;
68+
const id = ID_A;
69+
70+
const methods: ContractFunctionInteractionCallIntent[] = [
71+
// Mint methods
72+
{
73+
caller: alice,
74+
action: multiTokenContract.withWallet(wallet).methods.mint_to_private(owner, id, amt(100)),
75+
},
76+
{
77+
caller: alice,
78+
action: multiTokenContract.withWallet(wallet).methods.mint_to_public(owner, id, amt(100)),
79+
},
80+
81+
// Transfer methods
82+
{
83+
caller: alice,
84+
action: multiTokenContract.withWallet(wallet).methods.transfer_private_to_public(owner, bob, id, amt(10), 0),
85+
},
86+
{
87+
caller: alice,
88+
action: multiTokenContract.withWallet(wallet).methods.transfer_private_to_private(owner, bob, id, amt(10), 0),
89+
},
90+
{
91+
caller: alice,
92+
action: multiTokenContract.withWallet(wallet).methods.transfer_public_to_private(owner, bob, id, amt(10), 0),
93+
},
94+
{
95+
caller: alice,
96+
action: multiTokenContract.withWallet(wallet).methods.transfer_public_to_public(owner, bob, id, amt(10), 0),
97+
},
98+
99+
// Burn methods
100+
{
101+
caller: alice,
102+
action: multiTokenContract.withWallet(wallet).methods.burn_private(owner, id, amt(10), 0),
103+
},
104+
{
105+
caller: alice,
106+
action: multiTokenContract.withWallet(wallet).methods.burn_public(owner, id, amt(10), 0),
107+
},
108+
109+
// Partial notes methods
110+
{
111+
caller: alice,
112+
action: multiTokenContract.withWallet(wallet).methods.initialize_transfer_commitment(bob, owner),
113+
},
114+
{
115+
caller: alice,
116+
action: multiTokenContract
117+
.withWallet(wallet)
118+
.methods.transfer_private_to_commitment(owner, id, commitments[0], amt(10), 0),
119+
},
120+
{
121+
caller: alice,
122+
action: multiTokenContract
123+
.withWallet(wallet)
124+
.methods.transfer_public_to_commitment(owner, id, commitments[1], amt(10), 0),
125+
},
126+
];
127+
128+
return methods.filter(Boolean);
129+
}
130+
131+
async teardown(context: MultiTokenBenchmarkContext): Promise<void> {
132+
await context.cleanup();
133+
}
134+
}

src/multitoken_contract/Nargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "multitoken_contract"
3+
authors = [""]
4+
compiler_version = ">=1.0.0"
5+
type = "contract"
6+
7+
[dependencies]
8+
aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v5.0.0-rc.2", directory = "noir-projects/aztec-nr/aztec" }
9+
compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v5.0.0-rc.2", directory = "noir-projects/aztec-nr/compressed-string" }
10+
multitoken_authorization_contract = { path = "src/test/multitoken_authorization_contract" }
11+
generic_proxy = { path = "../generic_proxy" }

src/multitoken_contract/README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# MultiToken Contract
2+
3+
The `MultiToken` contract implements an ERC-1155-like multi-token with Aztec-specific privacy extensions. A single contract holds many fungible token ids, each with its own independent balance, and every balance can live in either a private or a public domain with seamless moves between the two.
4+
5+
Compared to the single-asset [`Token`](../token_contract/README.md), every balance-changing function takes an extra `id: Field` selecting the token, there is no `decimals` and no `total_supply`, and the on-chain event is `TransferSingle` (ERC-1155 naming) instead of `Transfer`.
6+
7+
## ARC-403: Authorization Hook
8+
9+
Like `Token`, this contract implements the optional ARC-403 authorization hook: when an `auth_contract` is configured, every transfer and burn calls it before mutating balances, and the operation reverts if the hook reverts. If `auth_contract` is the zero address, the hook is disabled and the token behaves as a plain multi-token. The interface is **id-bearing** — the hook receives the token id so policies can differ per id:
10+
11+
- `authorize_private(from, id, amount, selector)` — called by private-context functions.
12+
- `authorize_public(from, id, amount, selector)` — called by public-context functions.
13+
14+
| Function | Hook called |
15+
|----------|-------------|
16+
| `transfer_private_to_private` | `authorize_private` |
17+
| `transfer_private_to_public` | `authorize_private` |
18+
| `transfer_private_to_commitment` | `authorize_private` |
19+
| `transfer_public_to_private` | `authorize_private` |
20+
| `transfer_public_to_public` | `authorize_public` |
21+
| `transfer_public_to_commitment` | `authorize_public` |
22+
| `burn_private` | `authorize_private` |
23+
| `burn_public` | `authorize_public` |
24+
25+
Mints (`mint_to_private`, `mint_to_public`, `mint_to_commitment`) are **not** hooked — minting is already gated by the `minter` address set at construction.
26+
27+
## TransferSingle Events
28+
29+
A public `TransferSingle { from, to, id, amount }` event is emitted only on operations whose token id is already revealed on-chain (any public-balance write or public commitment completion). Fully-private operations emit nothing, since an id-bearing event would leak the token id.
30+
31+
| Operation | Event Pattern |
32+
|-----------|---------------|
33+
| Mint to public | `TransferSingle(0x0, recipient, id, amount)` |
34+
| Mint to commitment | `TransferSingle(0x0, PRIVATE_ADDRESS, id, amount)` |
35+
| Burn from public | `TransferSingle(from, 0x0, id, amount)` |
36+
| Public-to-public | `TransferSingle(from, to, id, amount)` |
37+
| Public-to-commitment | `TransferSingle(from, PRIVATE_ADDRESS, id, amount)` |
38+
| Public-to-private | `TransferSingle(from, PRIVATE_ADDRESS, id, amount)` |
39+
| Private-to-public | `TransferSingle(PRIVATE_ADDRESS, to, id, amount)` |
40+
| Mint to private / Burn from private | _(no public events)_ |
41+
| Private-to-private / Private-to-commitment | _(no public events)_ |
42+
43+
**Sentinel values:** `0x0` denotes mint origin (`from`) or burn destination (`to`), following ERC-1155. `PRIVATE_ADDRESS` (sha224 of `"PRIVATE_ADDRESS"`) denotes the private side of a balance change when the counterpart cannot be revealed.
44+
45+
## Storage Fields
46+
47+
- `name: FieldCompressedString`: Token collection name (compressed).
48+
- `symbol: FieldCompressedString`: Token collection symbol (compressed).
49+
- `private_balances: Owned<MultiBalanceSet>`: A single private note set. Each `MultiTokenNote` self-describes its token id; per-owner scoping is `private_balances.at(owner)`, and per-id operations select notes where `token_id == id`.
50+
- `public_balances: Map<Field, Map<AztecAddress, u128>>`: Public balances keyed by token id, then owner.
51+
- `minter: AztecAddress`: Account permitted to mint any token id.
52+
- `auth_contract: AztecAddress`: ARC-403 authorization contract address (zero address disables the hook).
53+
54+
## Function Reference
55+
56+
All addresses are `AztecAddress`; `id` is a `Field`, `amount` is a `u128`, and `nonce` (used for authwit) is a `Field`.
57+
58+
### Initializer
59+
60+
- `constructor_with_minter(name: FieldCompressedString, symbol: FieldCompressedString, minter, auth_contract)` — Initializes the multi-token with a minter and an optional ARC-403 auth contract.
61+
62+
### Private Functions
63+
64+
- `transfer_private_to_private(from, to, id, amount, nonce)` — Moves `amount` of `id` between private balances. Fully private (no event, no public effect).
65+
- `transfer_private_to_public(from, to, id, amount, nonce)` — Spends private notes and enqueues a public credit to `to`.
66+
- `transfer_private_to_commitment(from, id, commitment, amount, nonce)` — Spends private notes and completes an already-initialized commitment with `(id, amount)`.
67+
- `transfer_public_to_private(from, to, id, amount, nonce)` — Enqueues a public debit of `from` and emits a private note to `to`.
68+
- `initialize_transfer_commitment(to, completer) -> Field` — Creates a partial note (privacy entrance) to be completed by later transfers/mints. Id-agnostic: the completer binds `id` and `amount`.
69+
- `mint_to_private(to, id, amount)` — Minter mints `id` into a private balance. Fully private.
70+
- `burn_private(from, id, amount, nonce)` — Burns `id` from a private balance. Fully private.
71+
72+
### Public Functions
73+
74+
- `transfer_public_to_public(from, to, id, amount, nonce)` — Moves `amount` of `id` between public balances.
75+
- `transfer_public_to_commitment(from, id, commitment, amount, nonce)` — Debits `from`'s public balance and completes a commitment prepared by `initialize_transfer_commitment`.
76+
- `mint_to_public(to, id, amount)` — Minter mints `id` into a public balance.
77+
- `mint_to_commitment(id, commitment, amount)` — Minter finalizes a mint into a commitment.
78+
- `burn_public(from, id, amount, nonce)` — Burns `id` from a public balance.
79+
80+
### View Functions
81+
82+
- `balance_of_public(owner, id) -> u128` — Public balance of `owner` for `id`.
83+
- `name() -> FieldCompressedString` — Token collection name.
84+
- `symbol() -> FieldCompressedString` — Token collection symbol.
85+
- `get_minter() -> AztecAddress` — Authorized minter address.
86+
- `get_auth_contract() -> AztecAddress` — ARC-403 auth contract address (zero if disabled).
87+
88+
### Utility Functions
89+
90+
- `balance_of_private(owner, id) -> u128` — Off-chain helper that pages through the owner's notes filtered by `id` and sums their values. No on-chain or proving cost, and no fixed cap.

0 commit comments

Comments
 (0)