ERC20 Forwarder Contract V1 and V2 Description

This post describes the ERC20 forwarder contract V1 and V2 implementation that can be found here: https://github.com/anoma/anomapay-backend/tree/f298b748919d5398ac9f8b313d3ce033c943c6fd/contracts/src

Abbreviations

TRL: Token transfer resource logic
PA: Protocol adapter
FWD: ERC20 Forwarder contract

ERC20Forwarder (V1)

Initialization

The FWDv1 receives the following constructor arguments:

  • The PAv1 address being allowed to call the forwardCall method.
  • The TRLv1 logic reference that are allowed to trigger the calls via external payload blobs.
  • The emergency committee address that can set an emergency caller address once if the PAv1 has been stopped (because of an Anoma or RISC0 vulnerability).

Wrap Case

Calltrace

Pre-requisite: Owner has approved the Permit2 contract for the ERC20 token.

flowchart LR
TRLv1["eph., consumed TRLv1"]  --externalPayload--> PAv1 --forwardCall(Wrap,...)--> FWDv1 --permitWitnessTransferFrom()--> Permit2 --transferFrom()--> ERC20

External Payload

Triggering resource: ephemeral consumed TRLv1 resource

  • The CallType.Wrap enum value

  • The ERC20 token address

  • The owner address to pull to transfer funds from

  • The amount to transfer

  • Additional Permit2-related-related data

    • An unordered nonce for replay protection
    • The signature expiration deadline
    • The action tree root witness (see permitWitnessTransferFrom() in the SignatureTransfer | Uniswap docs)
  • the Permit2 ECDSA signature by the owner over the above data (except the call type) being domain separated by

Checks

ERC20Forwarder
  • Caller must be PAv1 (or the emergency caller set by the emergency committee)

  • Type-overflow check

    • resource.quantity in Rust is u128
    • Permit2 and ERC20 amounts are uint256
Permit2

Checks done in permitWitnessTransferFrom

  • Check that the signature has not expired (timestamp < deadline)
  • Check that the nonce had not been used
  • Check that the signature is valid by
    • reconstructing the message hash from the data
    • verifying the owner by
      • recovering the owner address in the case of an EOA being the owner
      • calling ERC1271.isValidSignature in case of an contract being the owner
ERC20
  • transferFrom call
    • Check that caller has been approved with a large enough allowance (e.g., type(uin256).amx) (here the permit2 contract)
    • check that the balance is sufficient

Unwrap Case

Calltrace

flowchart LR
TRLv1["eph. created TRLv1"] --externalPayload--> PAv1 --forwardCall(Unwrap,...)--> FWDv1 --transfer()--> ERC20

External Payload

Triggering resource: ephemeral created TRLv1 resource

  • The CallType.Unwrap enum value
  • The ERC20 token address
  • The owner address to pull to transfer funds from
  • The amount to transfer

Checks

ERC20Forwarder
  • Caller must be PAv1 (or the emergency caller).
ERC20
  • transfer call

    • check that the balance is sufficient

ERC20ForwarderV2 Draft

In general, an ERC20 forwarder V2 contract must adhere to the Anoma v2 protocol specs, fixing the vulnerability. Since these are not known yet, this is a draft.

Initialization

The FWDv2 receives the following constructor arguments:

  • The PAv2 address being allowed to call the forwardCall method
  • The TRLv2 logic reference that are allowed to trigger the calls via external payload blobs
  • The emergency committee address that can set an emergency caller once if the PAv2 has been stopped (because of an Anoma or RISC0 vulnerability).
  • The FWDv1 contract address that funds are migrated from.

Wrap & Unwrap

The wrap and unwrap cases are the same as described in the V1 Wrap and V1 Unwrap case. They are currently inherited from the ERC20Forwarder (V1).

Migrate Case

Calltrace

flowchart LR
TRLv1["eph., consumed TRLv2"]  --externalPayload--> PAv2 --forwardCall(MigrateV1,...)--> FWDv2--!isNullifierContained(nf)--> PAv1["PAv1 (stopped )"]
FWDv2 --emergencyForwardCall(Unwrap,...)-->
FWDv1 --transfer()--> ERC20

External Payload

Triggering resource: ephemeral, consumed TRLv2 resource

  • The CallTypeV2.MigrateV1 enum value
  • The ERC20 token address
  • The amount to transfer
  • The nullifier of the resource to migrate
  • The latest commitment tree root v1 of the stopped PAv1
  • The logic ref v1
  • The forwarder address v1

Checks

ERC20ForwarderV2
  • Caller must be PAv2 (or the emergency caller).
  • Check that the nullifier is not included in the PAv1 nullifier set.
  • Check that the nullifier is not included in the FWDv2 nullifier set.
  • Check that the commitment tree root v1 in the payload is the latest root of PAV1
  • Check that the logic reference in the payload is the one stored in FWDv1
  • Check that the forwarder address in the payload is the address of FWDv1

For the subsequent ERC20ForwarderV1 and ERC20 checks, see the V1 Unwrap Case.