# ARC-403: AuthToken

**URL:** <https://forum.aztec.network/t/arc-403-authtoken/7887>\
**Category:** General\
**Tags:** specs\
**Created:** [May 13, 2025, 7:48am UTC](https://forum.aztec.network/t/arc-403-authtoken/7887 "2025-05-13T07:48:38Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![wonderland](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/wonderland/32/2437_2.png) [@wonderland](https://forum.aztec.network/u/wonderland)\
**Post date:** [May 13, 2025, 7:48am UTC](https://forum.aztec.network/t/arc-403-authtoken/7887/1 "2025-05-13T07:48:38Z")

</div>

> Aztec Request for Comment 403, aka AuthToken, an extension of the ARC-20, named after the Forbidden Error.

## Motivation

Aztec’s blockchain infrastructure provides transaction confidentiality through its ARC-20 Token standard (aka Vanilla). Within this framework, users exercise granular control over their privacy, choosing between transferring private balances, visible only to intended recipients, or public balances, fully transparent to all network observers.

While this privacy-by-design approach fulfills Aztec’s core mission, many token issuers express a legitimate need for conditional privacy. Specifically, they seek mechanisms to ensure that transaction confidentiality privileges extend exclusively to validated participants, e.g. those who have completed KYC procedures, belong to permissible jurisdictions, or satisfy other compliance parameters.

This requirement becomes particularly critical in Aztec’s privacy-centric environment, where the attack surface for sybil manipulation is expansive. The blockchain’s privacy features mean that new account contracts remain invisible to observers, creating a significant challenge for implementing conventional compliance frameworks.

What’s needed is a cryptographic solution that preserves the integrity of Aztec’s privacy guarantees while enabling issuers to verify participant eligibility without compromising the token composability. Such a mechanism would allow legitimate regulatory requirements to coexist with privacy, rather than standing in opposition to it, while preserving full compatibility with ARC-20 specific apps.

## Specs

Preserving the ARC-20 interface is important for being fully compatible with ARC-20 specific apps (e.g. an AMM), hence, none of the transfer / minting / burning interfaces should be modified to apply the compliance layer to the Token.

Leveraging the versatile ARC-20 constructor (used to initialize, for example, capped supply tokens or mintable ones), an address is being added to it, called `transfer_authority`, that in case is different than `AztecAddress::zero()`, it will execute an external call to it each time the token is being transferred.

The ARC-403 specifies the calling interface to the Transfer Authority, as well as invites compliance framework creators to jump into the conversation of what the ideal interface should look like and design transfer authority contracts that can adapt to multiple use cases.

```rust
// on each Token Transfer...
if (!transfer_authority.eq(AztecAddress::zero())) {
    ComplianceCheck::at(transfer_authority)
        .authorize_transfer_{private/public}(
		        context.msg_sender(), 
		        from, 
		        amount
				).call(context);
}

```

The Transfer Authority will then have available the following information:

```rust
fn authorize_transfer_{private/public}(_sender: AztecAddress, _from: AztecAddress, _amount: u128) {
		context.msg_sender(); // the Token being transferred
		_sender; // the requestor of the token transfer (e.g. the AMM)
		_from; // the account from which the tokens are being transferred
		_amount; // the amount of tokens being transferred

		// only in private context 👇🏻
		unsafe { capsules::load(...) } // arbitrary data (e.g. a zk Proof)
}

```

**To be noticed** :

- The Transfer Authority is, within this (opinionated) Token implementation, an immutable AztecAddress, that may be an upgradeable contract
- The recipient of the transfer isn’t being passed in the call, yet whenever the recipient wants to spend its balance, it’ll need to be authorized ( **warning** : this can lead to bricked funds)
- The Transfer Authority may include any logic within the `authorize_transfer_*` call

**Open questions** :

- Is `authorize_transfer_public` needed?

- Is the transfer `nonce` needed?

### Possible implementations

This section is supposed to work as an inspiration to the logic that may live within the Transfer Authorization contracts. These are NOT to be implemented per-se, but showing what’s the possible scope of design of this feature.

- **Transfer accumulator** :

- **Leaking information** :

- **Signature by Authority** :

- **ZK Proving Identity** :

---

<div class="post-metadata">

**Author:** ![wei3erHase](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/wei3erhase/32/2263_2.png) [@wei3erHase](https://forum.aztec.network/u/wei3erHase)\
**Post date:** [March 9, 2026, 4:45pm UTC](https://forum.aztec.network/t/arc-403-authtoken/7887/2 "2026-03-09T16:45:45Z")

</div>

eyey, some updates on the 403, after some community survey we received the following feedback (a) `sender` (`self.msg_sender` of the Token contract – i.e. the AMM) doesn’t make sense to send, no policy can be added that would leverage “who the requestor” is, and could be circumvented by triggering the call via another operator, (b) sending `to` would be a nice-to-have for completeness.

as for (a), we agree on not finding a policy that could leverage the sender, but as for (b) we’ve found several caveats of adding `to` that are worth mentioning, and explaining why we propose to move forward without.

* * *

## ARC-403: Removing `to` from the Hook Interface

## The Problem with Keeping `to`

The hook currently exposes `(from, to, amount)`. For commitment-based transfers, `to` is sealed inside a hash preimage the sender never has. Three options exist for what to pass:

1. **`PRIVATE_ADDRESS_MAGIC_VALUE` sentinel** — the current approach. Hook authors receive a constant that means “recipient unknown.” Recipient-based policy is silently skipped. Any actor bypasses recipient gating by routing through a commitment. The interface promises `to` but delivers a lie.

2. **The commitment field cast as an address** — unique per transfer, but not a real address. No key material, no key registry entry. Any hook logic that emits a note or log to this “address” produces output no one can ever decrypt or discover. It looks like an address and behaves like a trap.

3. **The real `to` passed explicitly** — impossible for public commitment functions (`transfer_public_to_commitment`, `mint_to_commitment`) without destroying the privacy guarantee. For `transfer_private_to_commitment` it is also impossible: the sender only holds the opaque `Field`, not the preimage.

None of these give hook authors a usable recipient identity for commitment flows.

## Why a Commitment Creation Hook Does Not Fix This

`initialize_transfer_commitment(to, completer)` runs in private context and does know the real `to`. A dedicated hook here could gate commitment creation on the recipient. But:

- **`completer` is not `from`.** The completer is the address authorized to call the transfer function — an operator or intermediary. The actual balance source (`from`) is set later by the sender. Any sender-based policy added to the creation hook is circumventable by routing through a clean operator. Only recipient gating is reliably enforced.

- **Amount is unknown.** The amount is set at transfer time. Amount-based policy cannot be checked at creation time.

- **Bridging to public context is fragile.** The creation hook runs in private. For its approval to be visible to public completion, the auth contract must enqueue a public write of `commitment → authorized` to its own storage. This works mechanically but creates a staleness window: the policy is evaluated once at creation, then frozen. If a recipient is later sanctioned, their pre-authorized commitment remains valid.

The creation hook is therefore a narrow, single-purpose primitive: it enforces recipient eligibility at commitment creation time, nothing more. It does not restore a general-purpose `to` to the hook interface, and it does not solve the sender or amount enforcement gaps.

## Decision: Remove `to`

The hook interface becomes `authorize_private(from, amount)` and `authorize_public(from, amount)`.

`to` is removed because it cannot be provided consistently. Keeping it forces hook authors to handle a sentinel or a fake address for commitment flows, creating the illusion of recipient enforcement where none exists. A hook that appears to screen recipients but silently passes all commitment-based transfers is more dangerous than one that makes no claim about recipients at all.

Sender gating and amount gating remain fully enforceable across all flows — commitment or otherwise — because `from` and `amount` are always known at transfer time. Recipient gating on commitment flows is explicitly out of scope for the hook interface. Deployments that require it must implement it outside the hook, at the application layer.

* * *

# TL/DR

- policies that use `to` can always be circumvented by creating a partial-note commitment and transferring to the commitment
- hooking commitment creation doesn’t know who the `from` is (the account from where the funds will be deduced in the actual transfer)
- hooking commitment creation has fragile storage management, the hook is private but may be needed to consume in public
- a blocked-list address can receive funds (_we don’t have the recipient at policy checking_), but depending on the policy implemented it’d be never be able to move them

---

<div class="post-metadata">

**Author:** ![jasonxh](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/jasonxh/32/2507_2.png) [@jasonxh](https://forum.aztec.network/u/jasonxh)\
**Post date:** [March 10, 2026, 1:48am UTC](https://forum.aztec.network/t/arc-403-authtoken/7887/3 "2026-03-10T01:48:00Z")

</div>

Thanks for the thorough analysis, which explains pretty well the challenges of properly enforcing rules on `to` in transfers. However, I slightly disagree with the conclusion of simply dropping the ability to enforce any rules on receiving funds. I think there is still value in providing some (limited) ability to do so.

Along the line of what’s mentioned above, I believe an interface like this could make sense:

```auto
fn authorize_transfer_to_address_private(from: AztecAddress, amount: u128, to: AztecAddress);
fn authorize_transfer_to_address_public(from: AztecAddress, amount: u128, to: AztecAddress);

fn authorize_transfer_to_commitment_private(from: AztecAddress, amount: u128, to: Field);
fn authorize_transfer_to_commitment_public(from: AztecAddress, amount: u128, to: Field);

// To be enforced on `initialize_transfer_commitment`
fn authorize_new_transfer_commitment(to: AztecAddress, commitment: Field);

```

This exposes more low-level wiring of token transfers, which could make it look more daunting to implement an authorizer. To combat that, we could also provide helpers to implement a simple authorizer (that doesn’t care about `to`) on top of this.

While it still lacks the ability to reliably enforce on `to` as addresses, the low-level wiring does afford some new capabilities, e.g.

- Regulate commitment-based transfers on a high level. It can be quota based, or outright forbidden.
- Restrict someone’s ability to receive funds from direct transfers, or from creating new commitments.
- Perform private tracking on all transfers, either to address or to commitment, through private event emission and custom off-chain indexing.
  - This may sound scary, but it may be a requirement for certain tokens in some jurisdiction. At least it can be done privately such that only the token authority has access to said information.

---

<div class="post-metadata">

**Author:** ![nventuro](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/nventuro/32/1097_2.png) [@nventuro](https://forum.aztec.network/u/nventuro)\
**Post date:** [March 26, 2026, 5:11pm UTC](https://forum.aztec.network/t/arc-403-authtoken/7887/4 "2026-03-26T17:11:37Z")

</div>

(I’m not following the entire discussion, just commenting on the current state of partial note support in aztec-nr and future plans).

As of today, partial notes are fairly limited in their use. They **must** be created during a transaction, because they need to create what is called their ‘validity commitment’ nullifier. Additionally, they **must** be completed by a `completer` that is known at creation (typically a contract), and the completer **must** only complete each partial note once (or else notes will be lost).

We plan to introduce a second flow at some point in April, which we internally call ‘no-setup partial notes’. These would work as follows:

- Alice calls an `#[external(“utility”)]` contract function (i.e. runs a simulation), which returns a `PartialNote` object and creates an off-chain effect, notably a message with Alice as the recipient. This encrypted message contains the secret partial note preimage, which Alice requires in order to later spend the note.
- Alice calls the canoncal `offchain_receive` utility function in the contract with the produced offchain message. (in the near future her wallet may do this automatically because it’ll detect there is a message for Alice, and the wallet knows about Alice).
- Alice provides the `PartialNote` object to Bob via some off-chain channel, e.g. QR code, chat, email, etc.
- Bob can now call functions that take this `PartialNote`(e.g. token transfer functions), resulting in payments to Alice. Notably, Bob does not know Alice’s address. Only those actors that have seen the encrypted message content’s (i.e. Alice) know of the underlying recipient.
- Bob can complete the `PartialNote` repeatedly. All observers will know the recipient of all of these payments is the same one, but they won’t know the recipient’s address.
- The `PartialNote` object contains an internal `expiration_timestamp`. The `PartialNote` can only be completed before a certain point in time, after which it expires and completion is impossible. This completion window would likely not be huge, e.g. 24 hours.
- Bob is free to be naughty and mutate the `PartialNote` object, causing it to e.g. have a larger `expiration_timestamp`and enabling post-expiry completion. Alice will not receive the note. There is no mechanism to detect that Bob has not tampered with a `PartialNote` provided by Alice (the alternative would be to use a setup partial note, i.e. what exists today, or to come up with some signing scheme to validate authenticity).

---

<div class="post-metadata">

**Author:** ![shiqicao](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/shiqicao/32/2646_2.png) [@shiqicao](https://forum.aztec.network/u/shiqicao)\
**Post date:** [July 28, 2026, 3:45am UTC](https://forum.aztec.network/t/arc-403-authtoken/7887/5 "2026-07-28T03:45:06Z")

</div>

Thanks for pushing this forward — the hook is genuinely useful, and I’d rather  
argue about the shape now than after it ships.

What surprises me is that a change this central to the token standard is  
presented with a single design and no documented alternatives. Standards  
processes usually insist on an “alternatives considered” section precisely  
because an extension point is the hardest thing to change later — and this one  
is unusually final.

## Where the current design runs out of room

There are two distinct costs here, and they’re worth separating.

**The policy is welded to the deployed instance.** `auth_contract` is a  
`PublicImmutable` initialized in the constructor, and `initialize` emits an  
initialization nullifier — so it can be set exactly once and there is no  
setter. Consequences:

- One asset can carry exactly one policy, chosen before it has any holders.
- A live token can never gain, drop, or change a policy. Adding compliance to  
an existing asset means deploying a new token and migrating every holder.
- Two policies over the same asset are impossible. Wanting a second one means a  
second deployment — a different address, a different asset, not fungible with  
the first.
- The only escape is making the authorization contract itself upgradeable. But  
that relocates the mutability problem into a contract the ARC doesn’t  
specify, and hands whoever controls it unilateral authority over every  
transfer of that token, permanently.

**And the extension point itself is fixed at the source level.** Swapping in a  
different policy contract works, but changing the _shape_ or _number_ of  
extension points requires editing the token:

- **Fixed payload.** A policy needing the recipient, or arguments specific to  
one method, cannot get them. The thread notes `to` was deliberately removed —  
reasonable, but now frozen into every token that will ever use the hook.
- **Fixed call sites.** The hook fires on the seven transfer variants plus  
`burn_private` / `burn_public`. Mint is not hooked, so a policy that must  
gate issuance requires editing the token.
- **One slot.** Compliance plus transfer caps plus a fee rule must be merged  
into a single contract, or the token grows more call sites.
- **It already didn’t generalize.** MultiToken could not reuse the interface  
and defines its own `MultiTokenAuthorizationContract` taking  
`(from, id, amount, selector)`. Same concept, second interface, second token  
contract — the pattern repeating at n = 2.

So the combinatorics aren’t per implementation, they’re per concern, and they  
are paid twice: once in forked source per combination of concerns, and again in  
deployed assets, because every policy choice mints a distinct, non-fungible  
token. Given Noir has no dynamic dispatch, both costs land on users of the  
standard rather than on its authors.

## Alternative: composition instead of hooks

An **authenticator** is a contract that implements the full ARC-20 interface  
itself, applies its policy per method, and forwards to an inner token. The  
inner token accepts state-changing calls only from a registered set of  
authenticators.

The absence of dynamic dispatch is exactly why this works: the interface being  
called is the _fixed_ one (ARC-20), so the wrapper compiles against the  
standard rather than against any policy. Nothing needs forking in either  
direction.

1. No per-policy fork. One canonical token implementation serves every policy,  
instead of a fork per combination of concerns.
2. One authenticator implementation serves many tokens.
3. Concerns compose by stacking wrappers — linear, not a fork per combination.
4. The extension surface is the entire ARC-20 interface, mint included, rather  
than a fixed three-field payload.
5. Policy becomes deployable after the fact. A wrapper is a separate contract,  
so an authenticator can be introduced over an existing inner token without  
touching it and without re-issuing the asset — which the hook, being  
immutable per instance, fundamentally cannot do.

Worth noting the repo already ships `GenericProxy` with arity-enumerated  
`forward_private_0..N`, so forwarding wrappers are an accepted pattern here  
already. This is that shape, with the policy living in the wrapper.

### What this asks of the token

To be clear that this is not free on the token side either: for policy to be  
_enforced_ rather than merely offered, the token has to restrict who may call  
its state-changing methods — otherwise anyone routes around the authenticator  
by calling the token directly. Today it has no such mechanism: access control  
is an immutable `minter` for mints, plus authwit for spending `from`.

So this alternative is also an ask on the standard. The difference is what kind  
of ask:

- **ARC-403 asks for a specific extension point** — a policy interface with a  
fixed payload, wired into a fixed set of call sites, which had to be  
redefined once already for MultiToken.
- **Composition asks for a generic one** — “these addresses may call  
state-changing methods.” No payload, no per-method call sites, nothing that  
varies per asset type, and it is reusable for purposes beyond authorization.

That primitive inherits the same governance question, and it should be answered  
in the ARC rather than left to implementors: is the caller set immutable (in  
which case it repeats the rigidity above), owner-governed, or opt-in per user?  
My preference is per-user opt-in where the holder chooses which authenticator  
governs their balance, since it keeps the trust decision with the person whose  
funds are at stake.

Note the weaker form needs nothing at all: without any caller restriction,  
wrappers still work through authwit and give _opt-in_ policy — useful for  
user-chosen controls like spending limits, but not for issuer-enforced  
compliance. Only the enforced case requires the token’s cooperation.

## Tradeoffs I’d want discussed, not glossed over

I don’t think this is free, and the differences are worth arguing explicitly:

- **Trust boundary.** The inner token delegates authentication of `from` to the  
authenticator, so a registered authenticator can move any balance. ARC-403  
keeps the token itself authoritative. The registry becomes the critical  
governance surface.
- **Union, not intersection.** With multiple authenticators active over one  
balance, the effective policy is the weakest one. If that’s not wanted, the  
registry has to be single-entry or capability-scoped.
- **Identity and UX.** Users interact with the wrapper while notes live in the  
inner contract; wallets, PXE note discovery and explorers need a convention  
tying the two together. Two wrappers over one asset means two addresses for  
the same balance.
- **Cost.** One extra call frame per operation, which is real proving cost in  
private.

## Questions

1. Can the ARC document the alternatives considered, and why a hook was chosen  
over composition?
2. Is the immutable, single `auth_contract` intentional, versus a governed set?
3. Should mint be in scope for authorization?
4. Given MultiToken already needed a different payload, what’s the plan when  
the next asset type needs a fifth field?
5. Would a generic caller-restriction primitive on the token — “only these  
addresses may call state-changing methods” — be considered, either instead  
of the hook or alongside it? It is a smaller surface than a policy  
interface, and it does not need revisiting each time a new policy wants  
different inputs.

Minor, but related to treating this as a real interface: the hook interface  
currently ships from `src/token_contract/src/test/test_authorization_contract`  
— a test crate. If ARC-403 lands, that should be a first-class interface crate  
that implementors can depend on.

---

<div class="post-metadata">

**Author:** ![wei3erHase](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/wei3erhase/32/2263_2.png) [@wei3erHase](https://forum.aztec.network/u/wei3erHase)\
**Post date:** [August 4, 2026, 1:57pm UTC](https://forum.aztec.network/t/arc-403-authtoken/7887/6 "2026-08-04T13:57:58Z")

</div>

> [@shiqicao](#):
>
> ## Questions
> 
> 1. Can the ARC document the alternatives considered, and why a hook was chosen  
> over composition?
> 2. Is the immutable, single `auth_contract` intentional, versus a governed set?
> 3. Should mint be in scope for authorization?
> 4. Given MultiToken already needed a different payload, what’s the plan when  
> the next asset type needs a fifth field?
> 5. Would a generic caller-restriction primitive on the token — “only these  
> addresses may call state-changing methods” — be considered, either instead  
> of the hook or alongside it? It is a smaller surface than a policy  
> interface, and it does not need revisiting each time a new policy wants  
> different inputs.

1. Yes they were considered, composition is not yet a thing so shared exact contract (+side contract) was prioritized, as the rest of the features (mintable / fixed supply deployments)
2. Yes, adding a setter requires also adding an owner
3. Minting is already gated through a trusted party
4. Auth can be extended, question there remains whether 1 auth per multitoken or per token ID
5. We tried to keep the base token exactly the same across implementations, remember users will have to download the artifact

The auth token mechanism was created with the design goal of enabling the mechanism without rewriting the base ARC20 (in the scenario of updating every few weeks this became quite useful dev experience), I agree that composing or copy pasting into a new token contract provides more flexibility. The `to` was left behind because is impossible to cover every edge case (partial notes fulfillment) leaving always a window open. It has its limitations, but the extensions showcase the way how even with those limitations some useful functionalities can be added. In an ideal world, we’d have a few auth artifacts (differently set) and users won’t have to get a new artifact for each token they trade, but in the case of custom needs, forking the token and adapting it may save 100k gates on recursion, plus give you access to the token storage while running auth related logic.

---

<div class="post-metadata">

**Author:** ![shiqicao](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/shiqicao/32/2646_2.png) [@shiqicao](https://forum.aztec.network/u/shiqicao)\
**Post date:** [August 4, 2026, 6:04pm UTC](https://forum.aztec.network/t/arc-403-authtoken/7887/7 "2026-08-04T18:04:40Z")

</div>

> [@wei3erHase](#):
>
> The auth token mechanism was created with the design goal of enabling the mechanism without rewriting the base ARC20

“without rewriting” in software engineering context doesn’t mean two pieces of code are the same char by char. “without rewriting the base ARC20”, do they have same class ids? They are completely different contracts if you look at the lower level, they “embedded” or “static linked” totally different auth contract! “without rewriting the base ARC20” is very superficial sales words.

---

<div class="post-metadata">

**Author:** ![jimjim](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/jimjim/32/2590_2.png) [@jimjim](https://forum.aztec.network/u/jimjim)\
**Post date:** [August 5, 2026, 6:19pm UTC](https://forum.aztec.network/t/arc-403-authtoken/7887/8 "2026-08-05T18:19:06Z")

</div>

My hot take is to do it like railgun did. Do all compliance off-chain and keep onchain neutral. It does not require any changes onchain, and very flexible. Bad actors can still make transactions, but their txs and withdraws will not be added to the offchain public db that shows the proofs of every tx being “compliant”.  
Nice thing is that you can always add more protocols later, swap out proof system or anything you want. Since it’s not deployed onchain anyway. And a CEX with stricter requirements can fork the code and make their own requirements.

Ofc is you require no bad actor to ever be able to make a tx on your token, this might not be strict enough. But tbh it does not need to be that strict, railgun already does it like this and it deters and de-anonymize bad actors enough that there are no real issues here.  
Being a bad actor and having every shielded tx and withdraw being labeled “no proof of innocence” is not a good look and wont get you far when laundering.  
Besides the flexibility you gain as a dev, gives you more levers: to improve ux, compatibility with other shielded aztec contracts and to adapt to prevent abuse.  
Besides it’s fork-able, respects user choice better, and there for far less centralized.
