[Idea] Anoma Protocol Adapter × NEAR Intents

Anoma Protocol Adapter × NEAR Intents

Note: This is an idea that we could prototype, this document does not make any claims about product market fit or user stories justifying demand. Claude Code & Gemini Flash assisted with this document through several iterations.

tl;dr

  • Product: a cross-chain shielded-value router where NEAR Intents handles the asset conversion and cross-chain routing, and the Anoma Protocol Adapter receives the final leg as a shielded commitment on the destination chain. (shielded settlement for near intents).
  • Two headline flows: transparent ZEC to shielded ETH (one user intent), and shielded ETH (pool A) to shielded ETH (pool B) across chains.
  • Note: privacy is a composite property here. The shielded pool hides identity at the endpoints, the NEAR Intents leg is transparent between them. This is an honest tradeoff, not a regression; the alternative (no cross-chain shielded routing) is worse.

The problem

A user holds ZEC (transparent, or shielded-in-Zashi with local unshielding available) and wants shielded ETH in an Anoma pool on Ethereum or Base. Or they have shielded ETH in a pool on mainnet and want the equivalent note in a pool on Base. Both require 5 to 6 manual steps today (unshield, bridge, swap, redeposit), each leaking info and requiring transient transparent balances on linked addresses.

“ZEC” below means transparent-pool ZEC. NEAR Intents’ PoA bridge speaks only to Zcash’s transparent pool; Sapling/Orchard is not in-protocol. Zashi’s shield/unshield is a local Zcash-internal step.

Reader beware, privacy at rest only, not in flight. Zooko Wilcox-O’Hearn’s standing caveat (e.g. Bankless, Jan 2026): “You can’t get privacy from value in flight, you can only get privacy from value at rest.” Do not expect end-to-end cross-chain privacy from this design. The NEAR Intents leg is transparent; amounts, timing, and source-to-destination linkage are all observable. What this integration does is preserve the shielded status of the source-side balance inside Zashi’s Zcash shielded pool and of the destination-side balance inside the Anoma shielded pool. The transfer connecting them is public.

Three naive approaches fail:

  1. Build cross-chain routing inside Anoma - Controllers not cooked.
  2. Conventional bridge plus DEX - 0 privacy, suboptimal routing.
  3. Build your own solver network - Competing with incumbents for liquidity.

NEAR Intents already solved the hard parts. In October 2025 ECC integrated NEAR Intents into Zashi, giving one-tap swaps between BTC/SOL/USDC/ETH and transparent ZEC (with optional on-device shielding after arrival), plus CrossPay for the reverse direction. Live and routing hundreds of millions into the Zcash ecosystem per NEAR’s public numbers.

The solution

Reading OmniBridge.sol L346–349 and the reference BridgeToken.mint confirms NEAR Intents withdrawals land as plain IERC20.safeTransfer(recipient, amount) with no external call. The bytes message parameter is discarded by the reference mint. FtWithdraw.msg is NEAR-side only. Arbitrary destination calldata is not supported.

A second finding constrains how commitments enter the tree. Reading ProtocolAdapter.sol, CommitmentTree.sol, and Types.sol confirms there is no external appendCommitment; the only state-mutating entry is execute(Transaction), and every commitment must come from an Action with verified RISC Zero proofs. Third-party contracts participate only as IForwarder whose forwardCall output is compared against a pre-committed expectedOutput. The Receiver plays that IForwarder role.

Given both findings, the architecture binds the deposit to a deterministic per-user address and settles via execute(Transaction) against an IForwarder Receiver:

  1. Client computes commitment C, derives CREATE2 DepositProxy address D from a user-chosen salt.
  2. Client or relayer submits registerIntent(salt, commitment, amount, expiry) on the destination chain, binding (D → commitment) before funds move.
  3. Client submits the NEAR Intents intent with plain destination address D.
  4. NEAR Intents settles; plain ERC-20 lands at D.
  5. Anyone submits a pre-built execute(Transaction) that invokes Receiver.forwardCall, which verifies pending[D].commitment == commitment and sweeps the DepositProxy, and the same Transaction’s compliance unit creates C. Front-running is impossible because (salt → commitment) was recorded before funds were routable; the expectedOutput check binds forwardCall success to the specific commitment.

Shielded-to-shielded runs this primitive forward and back. NEAR Intents provides swap, routing, solver competition, ZEC bridging, and MPC signing. Anoma provides shielded settlement via execute(Transaction). The new contract is a Shielded Deposit Receiver (~200 lines).

Architecture

                        ┌─ (1) register(salt, C, amt, expiry) ─┐
                        │                                      ▼
┌────────────┐          │              ┌─────────────────────────────┐
│USER CLIENT ├──────────┘              │ SHIELDED DEPOSIT RECEIVER   │
│(off-chain) │─(2) publish_intent──┐   │  + DepositProxy factory     │
│  C, salt,  │                     │   │  pending[D] = (C, amt, exp) │
│  D=CREATE2 │                     │   └──────┬────────────┬─────────┘
└────┬───────┘                     │          │ (5) pulls  │ (5) appends C
     │                             │          │ from D     │ to PA tree
     │                             ▼          ▼            ▼
     │                  ┌────────────────┐  ┌──────┐  ┌─────────────┐
     │                  │ NEAR INTENTS   │  │ Dep. │  │  ANOMA PA   │
     │                  │ intents.near   │  │Proxy │  │ (commitment │
     │                  │ + Omni/PoA     │  │  D   │  │  tree)      │
     │                  │   bridges      │  └──▲───┘  └─────────────┘
     │                  └────────┬───────┘     │
     │                           │ (4) plain   │
     │                           │ safeTransfer│
     │                           └─────────────┘
     │
     └─(3) relayer submits intent, bridge routes funds to D ─ ─ ─ ─
                                                                  ▲
                                                                  │
     (anyone can call settle(salt) once D is funded) ─── (6) ─────┘

The four components

  1. User client. Generates the commitment locally using the user’s nullifier key: commitment = Poseidon_many([logic_ref, label_ref, value_ref, nk_commitment, nonce, psi, quantity, is_ephemeral, rcm]), nullifier = Poseidon(nk, nonce, psi, commitment). Proofs are RISC Zero zkVM (the deployed PA verifies only RISC0). Client packages commitment, encrypted note ciphertext, and intent parameters, and submits to the Solver Relay.

  2. NEAR Intents. Live; no changes needed. Verifier at intents.near holds NEP-141 balances and settles via execute_intents. Withdrawals route through Omni Bridge (ETH, Base, Arbitrum, BNB, Solana, BTC) or PoA bridge (wider list including ZEC, LTC, BCH, DOGE, XRP, Tron, Sui, Aptos, Cardano, Starknet).

  3. Shielded Deposit Receiver and DepositProxy. Implements IForwarder, invoked by the PA during execute(Transaction):

    contract ShieldedDepositReceiver is IForwarder {
        struct Pending { bytes32 commitment; uint256 amount; uint64 expiry; uint128 tip; }
        mapping(address => Pending) public pending;
        address public immutable asset;
        address public immutable proxyImpl;
        address public immutable pa;
        bytes32 public immutable LOGIC_REF;
    
        function registerIntent(bytes32 salt, bytes32 commitment, uint256 amount, uint64 expiry, uint128 tip) external {
            address D = Clones.predictDeterministicAddress(proxyImpl, salt, address(this));
            require(pending[D].commitment == bytes32(0), "in-use");
            pending[D] = Pending(commitment, amount, expiry, tip);
        }
    
        function forwardCall(bytes32 logicRef, bytes calldata input) external returns (bytes memory) {
            require(msg.sender == pa && logicRef == LOGIC_REF, "unauthorized");
            (bytes32 salt, bytes32 commitment) = abi.decode(input, (bytes32, bytes32));
            address D = Clones.predictDeterministicAddress(proxyImpl, salt, address(this));
            Pending memory p = pending[D];
            require(p.commitment == commitment && block.timestamp <= p.expiry, "bad-binding-or-expired");
            require(IERC20(asset).balanceOf(D) >= p.amount, "underfunded");
            if (D.code.length == 0) Clones.cloneDeterministic(proxyImpl, salt);
            DepositProxy(D).sweepTo(pa, p.amount - p.tip);
            if (p.tip > 0) DepositProxy(D).sweepTo(tx.origin, p.tip);
            delete pending[D];
            return abi.encode(salt, commitment, p.amount);
        }
    
        function refund(bytes32 salt, address to) external { /* expiry-gated */ }
    }
    

    The tip makes Transaction submission a permissionless MEV-style opportunity; NEAR Intents solvers do not natively callback to destination chains, so routing the incentive through the Receiver is the clean answer. DepositProxy is an EIP-1167 clone with sweepTo guarded by onlyReceiver. Because (D → commitment) is recorded before the intent is submitted, an observer seeing D in the bridge call cannot substitute a different commitment; forwardCall reads immutable pending[D] and expectedOutput binds the flow atomically.

  4. Anoma Protocol Adapter. Reused unmodified. ProtocolAdapter.sol maintains a binary Merkle commitment tree (Poseidon leaves) and a nullifier set, and verifies RISC Zero proofs for logic, compliance, delta, and optional aggregation. Deposits require an ARM Transaction whose compliance unit consumes an ephemeral-kind resource and creates the new PositionNote, plus an externalPayload targeting the Receiver’s forwardCall. The ephemeral flag is enforced inside the RISC Zero compliance circuit, not on-chain.

What the deployed contracts actually allow

Verified by reading ProtocolAdapter.sol on Base (0x094FCC…dd84F8) and BSC (0xFC44b6…2B68): the contract loops over Action.logicVerifierInputs[].appData.externalPayload[] and dispatches each blob as a separate forwardCall. Multiple forwarder calls per execute() tx are supported, and multiple execute() txs per block are observed on Base. The Solidity imposes no per-block cap. A “singleton calldata carrier” convention may exist at the RM circuit layer in the naive case, but it is not enforced on-chain.

The deployed ERC20Forwarder is pinned to a single _LOGIC_REF and supports only Wrap/Unwrap via Permit2. Verified at ERC20Forwarder.sol:161-172: Unwrap decodes UnwrapData { address receiver } and does IERC20(token).safeTransfer({to: data.receiver, value: amount}) with no on-chain constraint on receiver. Flow 2 Step 2’s Unwrap can target any address, including a PoA bridge deposit address; the off-chain resource-logic circuit gates which receivers are legitimate.

Flow 1: transparent ZEC to shielded ETH

Client-side setup

User picks nk, nonce, randomness, target quantity. Client computes commitment per the Poseidon schema above, picks a 256-bit CSPRNG salt, derives D = CREATE2(proxyImpl, salt, receiver), and encrypts the note plaintext under the user’s viewing key.

Step 1: register on destination

Client submits one transaction on the destination chain: Receiver.registerIntent(salt, commitment, amount, expiry). This reserves D and binds it to commitment. Requires destination-chain gas (one-time UX cost; see open problems).

Step 2: submit intent

Client calls the Solver Relay (POST https://solver-relay-v2.chaindefuser.com/rpc, method quote) with:

  • defuse_asset_identifier_in: ZEC
  • defuse_asset_identifier_out: WETH on the destination chain
  • exact_amount_in: X ZEC
  • Destination address: D (plain address, no calldata)

Signs and submits via publish_intent. PoA bridge enforces a 1.0 ZEC minimum plus 0.2 ZEC fee; client enforces a matching minimum.

Step 3: solver settles

A winning solver executes two legs. Leg A sends the user’s t-addr ZEC to the PoA-bridge Zcash t-addr for their NEAR account, and the Verifier credits the user’s balance. Leg B: execute_intents transfers NEP-141 WETH from user to solver internally; solver triggers a Verifier withdrawal that bridges real WETH to D, a plain IERC20.safeTransfer(D, amount).

Step 4: settle on destination

Anyone submits a pre-built execute(Transaction) whose compliance unit creates C from an ephemeral input, and whose externalPayload invokes Receiver.forwardCall(logicRef, abi.encode(salt, commitment)). PA verifies proofs, calls forwardCall, Receiver checks pending[D].commitment == commitment, lazy-deploys the DepositProxy, sweeps WETH from D to the PA (minus the tip to tx.origin), and returns. If the returned bytes match expectedOutput, the commitment is appended in the same execute(). The shielded note exists and is spendable. The user submits themselves, or publishes for a relayer to earn the tip.

Failure modes

Intent expires pre-settle, and ZEC stays in user custody. Bridge leg stalls, and WETH eventually arrives at D; settle works anytime before expiry. expiry passes with funds still at D, and refund(salt, to) returns WETH to a user-specified address; caller must prove knowledge of salt.

Flow 2: shielded ETH (pool A) to shielded ETH (pool B)

User has a shielded ETH note in pool A on chain A (mainnet), wants an equivalent note in pool B on chain B (Base). Same PA architecture on both chains, distinct commitment trees.

Step 0: pre-register on destination

Client computes C_B, salt_B, D_B for pool B, and calls Receiver_B.registerIntent(salt_B, C_B, amount, expiry) on chain B. The binding is now recorded on-chain on the destination side.

Step 1: pre-sign the intent

Client pre-signs a NEAR Intents intent: “deposit X asset to WETH on chain B, destination = D_B”. Held client-side.

Step 2: consume the source note

An ARM action that consumes the shielded ETH note on pool A (nullifier plus RISC Zero compliance and logic proofs) and produces a calldata-carrier resource targeting chain A’s ERC20Forwarder with an Unwrap sending WETH to the PoA bridge deposit address. The deployed ProtocolAdapter permits multiple forwarder calls per execute(), so this is one tx.

Step 3: intent settles

Funds land on NEAR. The pre-signed intent becomes executable; a solver bridges WETH to D_B as a plain transfer.

Step 4: settle on chain B

Anyone submits the pre-built chain-B execute(Transaction) with forwardCall(logicRef, abi.encode(salt_B, C_B)) to Receiver_B. C_B lands in pool B’s tree; tip goes to the submitter.

Asynchrony budget

A few minutes end-to-end, not atomic. Custody hops (PoA, Verifier, solver, chain-B DepositProxy) are each bounded.

Proving: client-side or outsourced?

What needs proving

Every deposit requires an ARM Transaction with RISC Zero proofs; execute(Transaction) is the only path into the commitment tree. Flow 1 and Flow 2 Step 4 are minimal Transactions: ephemeral input consumed, PositionNote created, logic + compliance + delta proofs. Flow 2 Step 2 is heavier because it consumes a real shielded note (Merkle inclusion against a stored root, nullifier derivation, plus a spending-predicate logic proof). Verified by reading anoma/evm-protocol-adapter: the PA uses RISC Zero exclusively. Seconds to minutes on consumer hardware, faster with GPU.

The PA verifies only ZKPs

Any outsourcing must still produce a valid RISC Zero proof.

Bring-your-own-proof

The PA verifies any valid RISC Zero proof regardless of source. The proof is the unit of trust; the prover is interchangeable. The SDK is prover-agnostic:

  1. User-generated. Run anoma/arm-risc0 locally (laptop, home GPU, own server). No service dependency, no trust beyond own hardware. (note we are not referencing MASP).
  2. AnomaPay’s existing proving service. Production, on anoma/arm-risc0. SDK submits the witness, receives a valid proof. Convenient default for mobile.
  3. Bonsai. RISC Zero’s hosted service. Same output format.
  4. Any other RISC Zero prover. Friend’s machine, self-hosted enclave, community pool. All outputs interchangeable.

Witness handling is the only trust question: whoever receives nk can leak it or front-run spends. Users pick per their own threat model; the SDK ships sensible defaults but never constrains the choice.

Batching

GPU parallelism across users plus on-chain amortisation via the PA’s set-union composition (multiple users’ actions share one execute() tx). Each user still needs their own proof; no SNARK-style recursion collapses many proofs into one on the RISC0 path.

Minimum client responsibility

nk generation and custody, and commitment hashing (cheap). Everything else (RISC0 proving, note-ciphertext assembly, intent submission) is delegable.

Architecture impact

Every deposit-side settle and every source-side consumption requires a proof. The SDK is prover-agnostic per the section above. Mobile clients default to AnomaPay’s service; desktop users can prove locally.

Privacy properties

Hidden

  • The identity of the ultimate owner of the new shielded note inside either pool. The commitment C is an opaque hash; only the user knows the preimage.
  • The link between the user’s source-side identity (Zashi z-addr holding the pre-unshield ZEC) and the destination-side recipient (shielded note in pool B). Zashi’s local shield/unshield carries standard Zcash shielded-pool privacy independent of this integration; we extend that privacy to a shielded ETH endpoint rather than a transparent one.
  • Within each pool, all standard shielded-pool properties apply: anonymity set equals the set of notes in the pool; action timing correlation is the main residual leak (same as Zcash).

Visible

  • At the NEAR Intents layer: the intent amount, the source asset, the destination asset, the destination chain, the destination address (the Shielded Deposit Receiver), and the settlement transaction on intents.near. The Verifier’s on-chain ledger records the swap.
  • On the destination chain: the ETH transfer from the bridge or solver to the Shielded Deposit Receiver, the amount, and the commitment hash. An observer watching the receiver contract sees every deposit commitment and the amount that backs it.
  • The link between a specific depositShielded call on the destination chain and a specific NEAR Intents withdrawal is trivially observable by correlating the bridge’s output transaction.

The nuance

Hides who owns the final note but not that some user converted transparent ZEC to shielded ETH of amount X at time T. The privacy win is endpoint identity, not transaction graph. Consistent with the reader-beware note at the top.

Dark funds between bridge delivery and settle

Funds sit in the undeployed CREATE2 proxy D between bridge arrival and settle(salt). Harmless (funds safe, pending[D] on-chain), but third-party dashboards won’t attribute the balance until lazy deployment. The client’s portfolio view must read pending[D] to surface these.

Client mitigations

Commitment diversification (split across commitments, costs more fees); timing jitter on settle(salt) within the bridge attestation window; RPC hygiene (private-transport or split-RPC).

Opt-in compliance via viewing keys

User holds their viewing key and can voluntarily export a signed history disclosure; the protocol never holds disclosure-capable keys.

Open problems and honest limits

  1. Calldata in withdrawals, resolved negative. Verified by reading Near-One/omni-bridge and near/intents. The pre-register plus IForwarder settle is the response.
  2. PA accepts commitments only via execute(Transaction), resolved. Verified by reading ProtocolAdapter.sol, CommitmentTree.sol, Types.sol: no external appendCommitment, ephemeral flag enforced off-chain only. Receiver implements IForwarder; every deposit is an ARM Transaction. Relayers earn the tip for constructing and submitting them.
  3. ERC20Forwarder Unwrap recipient is user-settable, resolved. Verified at ERC20Forwarder.sol:161-172. Flow 2 Step 2 can target the PoA bridge deposit address directly.
  4. Destination-chain gas before first deposit. Would need to ship EIP-712 meta-tx registration; relayers reimbursed from the tip at settle.
  5. Nullifier-key entropy. SDK enforces 256-bit CSPRNG for nk and salt.
  6. Mainnet-only testing. intents.near has no testnet; real-mainnet costs.

Sources

1 Like