AIP-165: Friendly Interfaces

Aztec Improvement Proposal 165, named in reference to EIP-165, aka supportsInterface standard.

Motivation

In our involvement in Aztec Hacker Residency 2025, we (Wonderland) set our sails in 2 main missions, (1) have our Token be used across different apps in the testnet, and (2) make it upgradeable, to make it easier to iterate between future versions.

The testnet has been launched a few weeks ago, and the main paradigm shift about interacting with it was (a) the persistent storage (contracts would be deployed to never be forgotten), and (b) composability between different apps. To simplify the flow for this document, we’ll reduce the goal to the following:

:man_astronaut:t2: Mission Composability: Have Wonderland’s Token (upgradeable), be
supported in Nemi AMM’s smart contracts, and be able to prove transactions using Obsidion Wallet.

The fact that the Token is upgradeable (still to be tested) was a relief for the rush of deploying it, it was lacking public metadata getters (for name, symbol, and decimals), yet they could be added later. We decided to go with the version we had, and deploy 3 tokens for the AMM and Wallet to integrate, we released an NPM package containing the deployed Contract Artifact, and a GitHub release tag, so that both Noir and Javascript codebases could import and interact with it.

Noir composability

So far, easy cake, Developer Experience seemed to be seamless, only when trying to integrate the Aztec Noir codebase to the Wallet and AMM circuits, we faced the first few problems: Expected type AztecAddress, found type AztecAddress. We’ve been working on the latest stable version v0.85.0, while the other teams were working with v0.85.0-alpha-testnet.2 and v0.85.0-alpha-testnet.9 respectively.

Since the Aztec version was different, imports were taken from different Nargo folders, and causing issues at compilation time. The multiple imports that the Token has were being fetched and compared against the other codebases, and failing to compile.

Javascript composability

Then came Javascript, the shared NPM package has a truthful representation of the deployed Tokens, so, no problems were expected here. Only that the Token is upgradeable, and that the circuit can change in a future. We thought that, for every version of a Token, we’d release a new NPM package, when the Token was upgraded, the Wallet would detect the new Contract Class ID, and voilá, use the correct Artifact. But then a simple question arrises: how’s the Wallet supposed to be compatible with both version 1 and version 2 of the Token? Given that one may be upgraded and the other not. Should the Wallet import each NPM package separately? Or should the NPM package be incremental and include all previous versions to disponibilize them?

Improvement Proposal

Noir interfaces

The truth is that for the AMM to import the Token Noir circuit, it doesn’t require the multiple imports that the token has:

[package]
name = "token_contract"
authors = [""]
compiler_version = ">=1.0.0"
type = "contract"

[dependencies]
aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.85.0", directory = "noir-projects/aztec-nr/aztec" }
uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.85.0", directory = "noir-projects/aztec-nr/uint-note" }
balance_set = { path = "../libs/balance-set" }
authwit = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.85.0", directory = "noir-projects/aztec-nr/authwit" }
compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.85.0", directory = "noir-projects/aztec-nr/compressed-string" }
contract_instance_deployer = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.85.0", directory = "noir-projects/noir-contracts/contracts/protocol/contract_instance_deployer_contract" }

Importing the whole Token with it’s Nargo.toml file seams inefficient, and prone to problems, considering that the only thing the AMM needs, is “an interface”: it doesn’t need the whole transfer_private_to_private circuit, it just needs to know there’s a method whose raw signature can be derived from transfer_private_to_private((Field),(Field),u128,Field). That’s it. The whole Token Interface could have only 1 import (#[aztec]), and be matched with the AMM’s Nargo.toml file:

use aztec::macros::aztec;
#[aztec]
pub contract TokenInterface {
	use aztec::prelude::AztecAddress;
	
	fn transfer_private_to_private(
		_from: AztecAddress,
		_to: AztecAddress,
		_amount: u128,
		_nonce: Field
	) {} // NOTE: empty circuit!
	
	fn transfer_private_to_public(
		// ...

This setup reduces unnecessary imports (only aztec), and it’s everything the AMM needs to compile!

:construction_worker_man:t2: Should we require to import the whole Token contract, we should make sure that for every Aztec release, we publish a GitHub tag, so that Noir developers have a compatible version with their current one.

Javascript Artifacts

Now, whenever the AMM swaps TokenV1 for TokenV2, is up to the Wallet to create the proof that involves: (1) the Wallet’s Account Contract, (2) the AMM Contract, (3) the TokenV1, and (4) the TokenV2. This is handled through a central database that links an AztecAddress -> ContractClassId, and requires the Wallet’s PXE to have registered every ContractClassId, in order to create a valid proof for the circuit execution path.

  • (1) Should be self-provided, the Wallet’s repo should have the Contract Artifact
  • (2) Should be provided by the AMM (NPM package)
  • (3) & (4) Should be provided by us (NPM package)

The difficulty comes when the Wallet needs to import every past version of the Token NPM Package, named token_contract-Token.json, and figure out which Contract Class ID corresponds to. Moreover, when the upgrade from V1 to V2 happens, the Wallet should be smart enough to re-register the Token contract, with the new Artifact, before proceeding to generate a proof (else it will be invalid).

When thinking how a seamless integration would look like, the Wallet should ask the DEPLOYER_CONTRACT_ADDRESS (a pre-compiled contract that holds record of every address and their declared latest ContractClassId), and be able to fetch (from somewhere) the corresponding Artifact.

Different than the EVM, where the bytecode of the contracts can be fetched exclusively from the chain, in Aztec we have “bytecode commitments” onchain, to which we need the pre-image, in order to interact. Different than the EVM, where pre-image of bytecode usually can be explained with 1 to 10 (let’s say) Solidity files, Aztec contracts depend on +100 files to be tracked! #[aztec] itself already depends on infinite interlinked dependencies, where any malicious line could result in a privacy-leaking circuit. Different than the EVM, where smart contracts (used to) have a limit of 24kb, Aztec Artifacts can weight (for example for the Token contract) more than 4mb — x100 (or more) storage requirement for each contract!

:construction_worker_man:t2: Should we release incremental NPM packages (each version publishing all the previous Artifacts), we’ll very soon end up with gigabyte-sized packages to download!

The need for a centralized source of Artifacts (as Etherscan centralizes “smart-contract verification”) rises up. But facilitating downloading circuits to be run is a security hazard. The need for social score of Artifacts appears, tagging them as “trustworthy” because they appeared in some GitHub CI action, of some self-compiled package, using some valid Aztec dependency. Audit companies may also like to tag the codebase as safe, making the Wallet aware of the security score of each downloaded Artifact, to request the User to approve running the circuit in their local PXE.

Not facilitating this availability of circuits can cause the load of composability to fall in the Wallet’s shoulders, as they’ll have to (1) track every new Token version and add it to their codebase, and (2) be aware of every Token upgrade to support the new versions.

Of course, some due diligence must be made by the User, to make sure they’re not executing a privacy-leaking circuit (i.e. that emits to me their secrets), but expecting the User to go online to fetch every possible Artifact of the circuit execution he wants to interact with is naïve, and a terrible User Experience.

Closing Comments

Let’s remember where we started off: allowing the User to interact via a Wallet, swapping TokenA for TokenB in an AMM. And both TokenA and TokenB being quite simple and isolated tokens, none was executing external calls (as we plan for the ARC-403, aka AuthToken).

This simple transaction, requires a heavy load of complexity, that this AIP aims to simplify with (a) Contract Interfaces (avoiding Noir developers the mess of maintaining package versioning), and (b) a Centralized Source of Artifacts (avoiding JS developers or Users the need to find the correct Artifact for the ContractClassID they’re interacting with).

2 Likes

Great feedback from the Obsidion team IRL, raising this reasoning here to iterate on the proposal: JS Apps don’t need the full artifact to compile.

This means, they also, as when we import a contract from Noir, just need the method selectors to be called, if we create a Noir Interface (the contract with the same methods but empty logic inside) and create an artifact for it, then we could avoid falling into +5mb files to import in every app.

How would that work? Let’s remember the case: a User want’s to Swap in the AMM TokenA for TokenB.

  • The JS app calls the AMM.swap, this gets routed to the Wallet
  • This is a direct call, so the Wallet may need first to call DEPLOYER_CONTRACT_ADDRESS (AztecAddress::from_field(2)) to learn what’s the AMM ContractClassID.
  • The Wallet then calls the Artifacts API, with the ContractClassID to download the full artifact, in order to simulate the TX
  • In the simulation, the Wallet learns that the AMM calls TokenA and TokenB, then it needs to call Deployer Contract Address to fetch the ContractClassId, and Artifacts API to learn the full Artifacts and re-simulate
  • In the 2nd simulation, the wallet learns that TokenB calls (e.g.) a TransferAuthorizer contract, so it needs to fetch the Artifact from the API
  • Loop until the simulation is completed
  • Generate the proof, and send it back to the JS App
  • JS App handles the communication with the RPC
  • Voilá :sparkles: