Skip to content

Instantly share code, notes, and snippets.

@jonaprieto
Last active February 15, 2026 06:36
Show Gist options
  • Select an option

  • Save jonaprieto/4ef7e5c78d4b1df879dc99f7d13d15fa to your computer and use it in GitHub Desktop.

Select an option

Save jonaprieto/4ef7e5c78d4b1df879dc99f7d13d15fa to your computer and use it in GitHub Desktop.

End-to-End ERC20 Wrap & Swap on Sepolia

This tutorial walks through wrapping ERC20 tokens into Anoma shielded resources and executing an atomic swap between two parties — all on Ethereum Sepolia testnet with ZK proofs generated remotely via RISC Zero Bonsai.

Overview

The flow has four steps:

fund_wallet ──► wrap_erc20 (maker) ──► wrap_erc20 (taker) ──► swap_test
    │                  │                       │                    │
    │  Mint testnet    │  ERC20 → Anoma        │  ERC20 → Anoma    │  Atomic swap:
    │  USDC & DAI      │  resource (USDC)      │  resource (DAI)   │  USDC ↔ DAI
    │  via Aave        │  + ZK proof           │  + ZK proof       │  + 8 ZK proofs
    │  Faucet          │  + on-chain TX        │  + on-chain TX    │  + on-chain TX

What happens under the hood:

  1. fund_wallet — Mints testnet USDC and DAI via the Aave V3 Faucet on Sepolia.
  2. wrap_erc20 (x2) — For each token: approves Permit2, builds an Anoma resource (consumed ephemeral + created persistent), generates compliance and logic ZK proofs via Bonsai, and submits the wrap transaction to the Protocol Adapter.
  3. swap_test — Loads both wrap outputs, reconstructs the resources, builds cross-swap resources (maker's USDC → taker, taker's DAI → maker), generates 8 ZK proofs (2 compliance + 4 logic + 1 delta + 1 aggregation), and submits the atomic swap to the Protocol Adapter.

Prerequisites

  • Rust toolchain (rustup, cargo)
  • A BIP-39 seed phrase with Sepolia ETH for gas (~0.1 ETH is plenty)
  • API keys for Alchemy and RISC Zero Bonsai

0. Clone and checkout

The swap_app crate lives on the jonaprieto/swap-app branch:

git clone [email protected]:jonaprieto/anomapay-backend.git
cd anomapay-backend
git checkout jonaprieto/swap-app

1. Configure environment

Export the required environment variables in your shell (e.g. add to ~/.zshrc):

export WALLET_SEED="forward truth repeat truly claw syrup blossom task session garden pull ill"
export BONSAI_API_URL="https://bento.heliax.fyi"
export BONSAI_API_KEY="149G32a+L/Fw734WeL0Mz1nJjkU/scpAKXpud+8oLtM="
export ALCHEMY_API_KEY="4APYD3IZW3XSVMHpebKQ-"
export RPC_URL="https://eth-sepolia.g.alchemy.com/v2/4APYD3IZW3XSVMHpebKQ-"
export GALILEO_INDEXER_ADDRESS="http://localhost:4000"

Then reload: source ~/.zshrc

Note: WALLET_SEED is a BIP-39 mnemonic. The binaries derive the Ethereum wallet at index 0. Alternatively, set FEE_PAYMENT_WALLET_PRIVATE_KEY with a hex-encoded private key.

Important: The binaries read env vars via std::env::var — there is no automatic .env file loading. You must export the variables in your shell.

2. Build the binaries

cargo build -p swap_app \
  --bin fund_wallet \
  --bin wrap_erc20 \
  --bin swap_test

This compiles three binaries in target/debug/:

Binary Purpose
fund_wallet Mints testnet tokens via Aave Faucet
wrap_erc20 Wraps an ERC20 token into an Anoma resource
swap_test Executes an atomic swap between two wrapped resources

3. Fund the wallet

Mint testnet USDC (6 decimals) and DAI (18 decimals) from the Aave V3 Faucet:

cargo run -p swap_app --bin fund_wallet

Expected output:

=== AnomaPay Testnet Token Funder ===

Wallet: 0xe13F26693A549Bf034968D7107d9dC7C81CffCBa
Chain ID: 11155111 (Sepolia = 11155111)

Minting 10 USDC (10000000 base units) via Aave Faucet...
  TX: 0xc6baf8bd...
Minting 10 DAI (10000000000000000000 base units) via Aave Faucet...
  TX: 0xaab95a54...

=== Token addresses for wrap_erc20 ===
USDC (Aave): 0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8
DAI  (Aave): 0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357

Note the Aave token addresses — these are different from Circle's native Sepolia USDC. Use these addresses in the wrap steps.

4. Wrap USDC for the maker

Wrap 5 USDC into an Anoma resource:

ERC20_TOKEN="0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8" \
  WRAP_AMOUNT_WEI=5000000 \
  WRAP_OUTPUT=wrap_maker.json \
  cargo run -p swap_app --bin wrap_erc20

This performs the following sequence:

  1. Derives wallet from seed phrase
  2. Checks token balance and Permit2 approval
  3. Builds a resource pair: consumed ephemeral (tokens entering the system) + created persistent (the shielded Anoma resource)
  4. Signs a Permit2 witness transfer
  5. Generates ZK proofs via Bonsai (compliance + logic proofs)
  6. Submits the transaction to the Protocol Adapter on Sepolia
  7. Saves keychain + resource metadata to wrap_maker.json

Expected output:

=== AnomaPay ERC20 Wrap Tool ===

Wallet address: 0xe13F26693A549Bf034968D7107d9dC7C81CffCBa
Token: 0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8
Amount: 5000000 wei

Created resource commitment: 0xccfe43b3...
Generating ZK proofs via Bonsai (this may take a few minutes)...
Transaction generated and verified!

=== SUCCESS ===
Sepolia TX hash: 0x95283570...
Wrap output saved to: wrap_maker.json

5. Wrap DAI for the taker

Wrap 5 DAI into a second Anoma resource:

ERC20_TOKEN="0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357" \
  WRAP_AMOUNT_WEI=5000000000000000000 \
  WRAP_OUTPUT=wrap_taker.json \
  cargo run -p swap_app --bin wrap_erc20

Note: DAI has 18 decimals, so 5 DAI = 5000000000000000000 wei. USDC has 6 decimals, so 5 USDC = 5000000 wei.

6. Execute the atomic swap

With both wrap_maker.json and wrap_taker.json in place, run the swap:

MAKER_WRAP_OUTPUT=wrap_maker.json \
  TAKER_WRAP_OUTPUT=wrap_taker.json \
  cargo run -p swap_app --bin swap_test

The swap binary:

  1. Loads both wrap output JSON files
  2. Reconstructs the persistent resources and verifies commitments match
  3. Builds cross-swap resources: maker's USDC → taker, taker's DAI → maker
  4. Computes the action tree (Merkle tree of nullifiers and commitments)
  5. Signs the action tree root with both parties' authorization keys
  6. Generates 8 ZK proofs via Bonsai:
    • 2 compliance proofs (consumed persistent → created persistent)
    • 4 logic proofs (2 consumed + 2 created resources)
    • 1 delta proof (net balance conservation)
    • 1 aggregation proof (combining everything)
  7. Submits the swap transaction to the Protocol Adapter on Sepolia
  8. The RPC indexer scans historical blocks to build the commitment Merkle tree

Expected output:

=== AnomaPay Swap Test ===

Maker: 5000000 wei of token 0x94a9D9AC...
Taker: 5000000000000000000 wei of token 0xFF34B3d4...

Maker resource reconstructed (commitment matches)
Taker resource reconstructed (commitment matches)

Action tree root: 0xeedabe4d...
Both parties signed the action tree root.

Generating ZK proofs via Bonsai (this may take several minutes)...

=== SWAP SUCCESS ===
Sepolia TX hash: 0xb21fbcc1...

Consumed (maker): 0x94a9D9AC... 5000000 wei
Consumed (taker): 0xFF34B3d4... 5000000000000000000 wei
Created for taker: 0x94a9D9AC... 5000000 wei
Created for maker: 0xFF34B3d4... 5000000000000000000 wei

7. Verify the transaction

After the swap completes, you can verify it on Etherscan:

https://sepolia.etherscan.io/tx/<tx-hash>

Environment variable reference

Variable Required Description
WALLET_SEED Yes* BIP-39 mnemonic (alternative: FEE_PAYMENT_WALLET_PRIVATE_KEY)
RPC_URL Yes Ethereum Sepolia RPC endpoint
ALCHEMY_API_KEY Yes Alchemy API key (used by ForwarderConfig internally)
BONSAI_API_URL Yes RISC Zero Bonsai prover URL (https://bento.heliax.fyi)
BONSAI_API_KEY Yes RISC Zero Bonsai API key
GALILEO_INDEXER_ADDRESS wrap only Galileo indexer URL (e.g. http://localhost:4000). Required by wrap_erc20; the swap_test binary uses its own RPC-based indexer instead.
ERC20_TOKEN wrap only Token address to wrap (default: Sepolia WETH)
WRAP_AMOUNT_WEI wrap only Amount to wrap in smallest unit (default: 1000000000000000)
WRAP_OUTPUT wrap only Output JSON path (default: wrap_output.json)
MAKER_WRAP_OUTPUT swap only Path to maker's wrap output JSON (default: wrap_maker.json)
TAKER_WRAP_OUTPUT swap only Path to taker's wrap output JSON (default: wrap_taker.json)
INDEXER_RPC_URL No RPC for log queries during swap (default: https://ethereum-sepolia-rpc.publicnode.com)
PA_START_BLOCK No Block to start scanning PA events (default: 10130000)

Aave testnet token addresses (Sepolia)

Token Address Decimals
USDC 0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8 6
DAI 0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357 18

Contract addresses (Sepolia)

Contract Address
ERC20 Forwarder V1 0x0A62bE41E66841f693f922991C4e40C89cb0CFDF
Protocol Adapter 0xf152BBA809d6cba122579cee997A54B8F3FBa417
Permit2 0x000000000022D473030F116dDEE9F6B43aC78BA3
Aave V3 Faucet 0xC959483DBa39aa9E78757139af0e9a2EDEb3f42D

Troubleshooting

"server error Forbidden" during proof generation

  • Verify your BONSAI_API_KEY is correct. Watch out for O (letter) vs 0 (zero) and l (letter) vs 1 (one) in base64-encoded keys.
  • The Bonsai API URL should be https://bento.heliax.fyi (not any .internal URLs).

"HTTP error from reqwest" during proof generation

  • The Bonsai API URL may be unreachable. Verify network connectivity to https://bento.heliax.fyi.

"Insufficient token balance"

  • Run fund_wallet first to mint testnet tokens via the Aave Faucet.
  • Ensure you're using the Aave token addresses, not Circle's native Sepolia USDC.

"Commitment mismatch" during swap

  • The wrap_maker.json or wrap_taker.json file may be stale or corrupted. Re-run the wrap step.

Swap takes a long time

  • The RPC indexer scans all historical blocks from PA_START_BLOCK to find existing commitments. This can take a minute on the first run. You can set PA_START_BLOCK to a more recent block to speed things up.

"package swap_app not found"

  • Make sure you're on the jonaprieto/swap-app branch. The swap_app crate does not exist on main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment