An overview of the Protocol Adapter (PA-EVM) and AnomaPay ERC20 Forwarder smart contracts.
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
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
├── 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)
| 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.
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
- 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
- Tracks consumed resource nullifiers to prevent double-spending
- Any duplicate nullifier causes an immediate revert
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
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
| 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 |
| 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) |
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
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.
The ERC20Forwarder wraps/unwraps arbitrary ERC20 tokens in the AnomaPay application. It uses Uniswap Permit2 for gasless token approvals.
| 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) |
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
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
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)
Note: Both normal and emergency unwrap paths emit the same
Unwrappedevent — there is no on-chain distinction between them.
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;
}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
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
| 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 |
- 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)
- 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)
| 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 |
| 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 |