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
forwardCallmethod. - 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.Wrapenum 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
- The Chain ID
- The Permit2 contract address
- EIP-712 type- and namehash (see EIP-712: Typed structured data hashing and signing)
Checks
ERC20Forwarder
-
Caller must be PAv1 (or the emergency caller set by the emergency committee)
-
Type-overflow check
resource.quantityin Rust isu128Permit2and ERC20 amounts areuint256
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
transferFromcall- 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
- Check that caller has been approved with a large enough allowance (e.g.,
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.Unwrapenum 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
-
transfercall- 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
forwardCallmethod - 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.MigrateV1enum 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.