Skip to content

Instantly share code, notes, and snippets.

@jonaprieto
Created February 25, 2026 05:42
Show Gist options
  • Select an option

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

Select an option

Save jonaprieto/baba251e44a64406c300016b13765ba2 to your computer and use it in GitHub Desktop.
Anoma EVM Smart Contracts: PA-EVM & AnomaPay ERC20 Forwarder overview

Anoma EVM Smart Contracts

An overview of the Protocol Adapter (PA-EVM) and AnomaPay ERC20 Forwarder smart contracts.


What is the Protocol Adapter?

The PA-EVM is a Solidity smart contract that settles Anoma Resource Machine transactions on EVM-compatible chains.

It enables privacy-preserving, UTXO-like state transitions using RISC Zero zero-knowledge proofs.

"A driver allowing Anoma applications to be run on existing EVM-compatible chains." — specs.anoma.net


Resource Machine Primer

The Anoma Resource Machine (ARM) is a deterministic stateless machine that creates, composes, and verifies transactions.

  • Resources are the atomic units of state — immutable, created once, consumed once
  • System state = set of active (created but not yet nullified) resources
  • Unlike Ethereum's account model, ARM uses a UTXO-like model
Resource = (logic, label, quantity, value, ephemeral, nonce, nullifierKeyCommitment, randSeed)

Transaction Structure

Transaction
├── Action[]
│   ├── ComplianceUnit[] (each has 1 consumed + 1 created resource)
│   └── LogicInput[] (one per resource tag)
│       └── AppData
│           ├── ResourcePayload[]
│           ├── DiscoveryPayload[]
│           ├── ExternalPayload[]
│           └── ApplicationPayload[]
├── deltaProof        (proves the transaction is balanced)
└── aggregationProof  (optional recursive proof for gas savings)

Three Proof Types

Proof Purpose Verifying Key
Compliance Proves consumed → created resource transformation is valid Fixed (0x919e…314d)
Logic Verifies resource-specific business rules Variable (= logicRef, hash of the logic function)
Delta Proves transaction is balanced (∑ consumed = ∑ created) Keccak-256 of all tags

An optional Aggregation Proof can batch all compliance + logic proofs into one RISC Zero proof for gas savings.


PA-EVM Contract Architecture

classDiagram
    class ProtocolAdapter {
        +execute(Transaction)
        +simulateExecute(Transaction, bool)
        +emergencyStop()
        +isEmergencyStopped()
    }
    class CommitmentTree {
        +commitmentCount()
        +commitmentTreeDepth()
        -_addCommitment(bytes32)
        -_addCommitmentTreeRoot(bytes32)
    }
    class NullifierSet {
        +isNullifierContained(bytes32)
        +nullifierCount()
        -_addNullifier(bytes32)
    }

    ProtocolAdapter --|> CommitmentTree
    ProtocolAdapter --|> NullifierSet
    ProtocolAdapter --|> ReentrancyGuardTransient
    ProtocolAdapter --|> Ownable
    ProtocolAdapter --|> Pausable
Loading

On-Chain State

Commitment Tree (Merkle Tree)

  • Append-only SHA-256 Merkle tree of resource commitments
  • Stores historical roots so consumed resources can prove existence against past state
  • Dynamic depth expansion when capacity is reached

Nullifier Set

  • Tracks consumed resource nullifiers to prevent double-spending
  • Any duplicate nullifier causes an immediate revert

Execution Flow

flowchart TD
    TX[Transaction] --> INIT[Count tags, check even]
    INIT --> ACTIONS[For each Action]
    ACTIONS --> ATR[Compute Action Tree Root]
    ATR --> CU[For each ComplianceUnit]
    CU --> COMP[Verify Compliance Proof]
    COMP --> CONSUMED[Process Consumed Resource]
    CONSUMED --> N1[Check commitment tree root exists]
    CONSUMED --> N2[Verify logic proof]
    CONSUMED --> N3[Execute forwarder calls]
    CONSUMED --> N4[Add nullifier to NullifierSet]
    COMP --> CREATED[Process Created Resource]
    CREATED --> C1[Verify logic proof]
    CREATED --> C2[Execute forwarder calls]
    CREATED --> C3[Add commitment to CommitmentTree]
    CU --> DELTA_ACC[Accumulate unit delta]
    DELTA_ACC --> GLOBAL[Global Verification]
    GLOBAL --> DP[Verify Delta Proof]
    GLOBAL --> AP[Verify Aggregation Proof if present]
    AP --> FINAL[Store root + Emit TransactionExecuted]
    DP --> FINAL
Loading

Events Emitted

flowchart LR
    subgraph Calldata
        TX[Transaction]
        ACT[Action]
        LI[LogicInput]
        CU[ComplianceUnit]
    end

    subgraph Events
        TE[TransactionExecuted]
        AE[ActionExecuted]
        RP[ResourcePayload]
        DP2[DiscoveryPayload]
        EP[ExternalPayload]
        APP[ApplicationPayload]
    end

    TX --> TE
    ACT --> AE
    LI --> RP
    LI --> DP2
    LI --> EP
    LI --> APP
Loading
Event Indexed Fields Data
TransactionExecuted tags[], logicRefs[]
ActionExecuted actionTreeRoot, actionTagCount
ResourcePayload tag index, blob
DiscoveryPayload tag index, blob
ExternalPayload tag index, blob
ApplicationPayload tag index, blob

Data Scattering (for Indexers)

To build this entity… You need events… Plus calldata for…
Transaction TransactionExecuted deltaProof, aggregationProof
Resource TransactionExecuted + ResourcePayload
Action ActionExecuted
ComplianceUnit Everything (not in events)
LogicInput Everything (not in events)
Payloads Payload events deletionCriterion (if you want it)

Indexer Entity Graph

erDiagram
    Transaction ||--|{ Action : contains
    Action ||--|{ ComplianceUnit : contains
    Action ||--|{ Resource : references
    ComplianceUnit ||--|| Resource : "consumed"
    ComplianceUnit ||--|| Resource : "created"
    Resource ||--|{ LogicInput : has
    Resource ||--o{ DiscoveryPayload : has
    Resource ||--o{ ApplicationPayload : has
Loading

The Forwarder Pattern

Forwarders let resources interact with external EVM state while inside a protocol adapter transaction.

Protocol Adapter ── forwardCall(logicRef, input) ──► Forwarder ──► External Contract

Interface:

interface IForwarder {
    function forwardCall(bytes32 logicRef, bytes memory input)
        external returns (bytes memory output);
}

The PA verifies that the actual output matches the expected output encoded in the resource's externalPayload.


AnomaPay ERC20 Forwarder

The ERC20Forwarder wraps/unwraps arbitrary ERC20 tokens in the AnomaPay application. It uses Uniswap Permit2 for gasless token approvals.

Two Operations

Operation What happens Event
Wrap Pull tokens from user via Permit2 → deposit into forwarder Wrapped(token, from, amount)
Unwrap Send tokens from forwarder → to receiver via SafeERC20 Unwrapped(token, to, amount)

ERC20 Forwarder Contract Hierarchy

classDiagram
    class ForwarderBase {
        +forwardCall(bytes32, bytes)
        +getProtocolAdapter()
        +getLogicRef()
        #_forwardCall(bytes)*
        -_PROTOCOL_ADAPTER
        -_LOGIC_REF
    }
    class EmergencyMigratableForwarderBase {
        +forwardEmergencyCall(bytes)
        +setEmergencyCaller(address)
        +getEmergencyCaller()
        #_forwardEmergencyCall(bytes)*
        -_EMERGENCY_COMMITTEE
        -_emergencyCaller
    }
    class ERC20Forwarder {
        #_forwardCall(bytes)
        #_forwardEmergencyCall(bytes)
        -_wrap(token, amount, input)
        -_unwrap(token, amount, input)
    }
    class ERC20ForwarderV2 {
        -_migrateV1(token, amount, input)
    }
    class ERC20ForwarderV3 {
        -_migrateV1(...)
        -_migrateV2(...)
    }

    ForwarderBase <|-- EmergencyMigratableForwarderBase
    EmergencyMigratableForwarderBase <|-- ERC20Forwarder
    ERC20Forwarder <|-- ERC20ForwarderV2
    ERC20ForwarderV2 <|-- ERC20ForwarderV3
Loading

Call Flow: Normal Path

sequenceDiagram
    participant PA as Protocol Adapter
    participant FW as ERC20Forwarder
    participant P2 as Permit2
    participant TOK as ERC20 Token
    participant USER as User / Receiver

    Note over PA,FW: WRAP (deposit tokens)
    PA->>FW: forwardCall(logicRef, input)
    FW->>FW: validate caller = PA, logicRef matches
    FW->>P2: permitWitnessTransferFrom(permit, transferDetails, owner, witness, sig)
    P2->>TOK: transferFrom(owner, forwarder, amount)
    FW-->>FW: emit Wrapped(token, from, amount)
    FW-->>PA: return output

    Note over PA,FW: UNWRAP (withdraw tokens)
    PA->>FW: forwardCall(logicRef, input)
    FW->>FW: validate caller = PA, logicRef matches
    FW->>TOK: safeTransfer(receiver, amount)
    TOK->>USER: transfer
    FW-->>FW: emit Unwrapped(token, to, amount)
    FW-->>PA: return output
Loading

Call Flow: Emergency Path

When the RISC Zero verifier or protocol adapter is emergency stopped, tokens can still be recovered.

sequenceDiagram
    participant EC as Emergency Committee
    participant CALLER as Emergency Caller
    participant FW as ERC20Forwarder
    participant TOK as ERC20 Token
    participant USER as Receiver

    Note over EC,FW: One-time setup
    EC->>FW: setEmergencyCaller(callerAddress)
    FW->>FW: verify PA is stopped, caller not already set

    Note over CALLER,USER: Emergency unwrap
    CALLER->>FW: forwardEmergencyCall(input)
    FW->>FW: verify caller = emergencyCaller
    FW->>FW: verify PA is emergency stopped
    FW->>FW: _forwardCall(input) — same as normal unwrap
    FW->>TOK: safeTransfer(receiver, amount)
    TOK->>USER: transfer
    FW-->>FW: emit Unwrapped(token, to, amount)
Loading

Note: Both normal and emergency unwrap paths emit the same Unwrapped event — there is no on-chain distinction between them.


Permit2 Integration (Wrap)

The wrap operation uses Uniswap's Permit2 with a custom witness containing the actionTreeRoot:

struct Witness {
    bytes32 actionTreeRoot;
}

This binds the ERC20 approval to a specific Anoma action, preventing replay across different transactions.

struct WrapData {
    uint256 nonce;
    uint256 deadline;
    address owner;
    bytes32 actionTreeRoot;
    bytes32 r;
    bytes32 s;
    uint8 v;
}

Migration Strategy (V2 / V3)

When the protocol adapter needs to be upgraded, the forwarder supports cross-version migration:

flowchart LR
    V1[ERC20Forwarder V1] -->|emergency stop V1| V2[ERC20Forwarder V2]
    V2 -->|emergency stop V2| V3[ERC20Forwarder V3]

    V1 -.->|MigrateV1: pull tokens via emergency unwrap| V2
    V1 -.->|MigrateV1: via V2 delegation| V3
    V2 -.->|MigrateV2: pull tokens via emergency unwrap| V3
Loading

Migration validates:

  • Resource not already consumed in the source version (nullifier check)
  • Commitment tree root matches the source version's final state
  • Logic reference and forwarder address match

Access Control Summary

Function Who can call Conditions
forwardCall Protocol Adapter only logicRef must match
forwardEmergencyCall Emergency Caller only PA must be stopped, caller must be set
setEmergencyCaller Emergency Committee only PA must be stopped, can only be set once
execute (PA) Anyone Transaction must pass all proof checks
emergencyStop (PA) Owner only Irreversible

Security Highlights

Protocol Adapter

  • Reentrancy protection (transient storage guard)
  • Permanent emergency stop (owner-only, irreversible)
  • RISC Zero proof verification for compliance, logic, delta
  • Forwarder output matching (expected vs actual)
  • Audited by Informal Systems and Nethermind (2025)

ERC20 Forwarder

  • Balance validation before/after every transfer (catches fee-on-transfer tokens)
  • Input length validation (prevents malformed calldata)
  • One-time emergency caller setup (cannot be changed)
  • SafeERC20 for non-standard token compatibility
  • Immutable references (PA, logicRef, committee cannot change)
  • Audited by Informal Systems (Dec 2025)

Contract Inventory

PA-EVM (pa-evm/contracts/src/)

File Purpose
ProtocolAdapter.sol Main entry point — verifies and executes RM transactions
Types.sol Core type definitions (Resource, Transaction, Action, etc.)
state/CommitmentTree.sol Append-only Merkle tree of resource commitments
state/NullifierSet.sol Double-spend prevention via nullifier tracking
libs/proving/Compliance.sol Compliance proof verification
libs/proving/Logic.sol Resource logic proof verification
libs/proving/Delta.sol Transaction balance proof (secp256k1 ECDSA)
libs/proving/Aggregation.sol Optional batched proof verification
libs/MerkleTree.sol Dynamic-depth SHA-256 Merkle tree
libs/RiscZeroUtils.sol Solidity → RISC Zero journal encoding
libs/TagUtils.sol Tag collection and counting
interfaces/IForwarder.sol Forwarder interface for external interop

ERC20 Forwarder (anomapay-erc20-forwarder/contracts/src/)

File Purpose
ERC20Forwarder.sol Wrap/unwrap ERC20 tokens via Permit2
ERC20ForwarderPermit2.sol Permit2 witness type definitions (EIP-712)
bases/ForwarderBase.sol Base: caller validation, reentrancy guard
bases/EmergencyMigratableForwarderBase.sol Base: emergency committee + caller setup
drafts/ERC20ForwarderV2.sol V2: adds V1→V2 migration
drafts/ERC20ForwarderV3.sol V3: adds V1→V3 and V2→V3 migration

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment