- Published on
ERC7984Authorization: Extending ERC-3009 for Confidential Tokens
Abstract
A set of functions to enable meta-transactions for ERC-7984 confidential tokens via EIP-712 signatures. Enables gasless transfers while preserving balance confidentiality through FHE (Fully Homomorphic Encryption). Supports both plaintext amount authorization (encrypted on-chain) and pre-encrypted amount authorization.
Motivation
ERC-3009 enables gasless transfers for ERC-20 tokens, but cannot be directly applied to confidential tokens (ERC-7984) due to:
- Balance confidentiality: Transfers cannot revert on insufficient balance without leaking information
- Amount privacy: Calldata visibility exposes transfer amounts
- Success verification: The submitter cannot synchronously verify transfer success
This specification adapts ERC-3009 for confidential tokens by:
- Returning an encrypted
transferredamount (actual amount moved, or zero if insufficient balance) - Granting the submitter decryption rights to verify success asynchronously
- Offering both plaintext and encrypted amount variants for different privacy requirements
Specification
Events
event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);
event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce);
Type Hashes
// keccak256("ConfidentialTransferWithAuthorization(address from,address to,uint64 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant CONFIDENTIAL_TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x...;
// keccak256("ConfidentialReceiveWithAuthorization(address from,address to,uint64 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant CONFIDENTIAL_RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0x...;
// keccak256("ConfidentialTransferWithAuthorizationEncrypted(address from,address to,bytes32 encryptedAmountCommitment,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant CONFIDENTIAL_TRANSFER_WITH_AUTHORIZATION_ENCRYPTED_TYPEHASH = 0x...;
// keccak256("ConfidentialReceiveWithAuthorizationEncrypted(address from,address to,bytes32 encryptedAmountCommitment,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant CONFIDENTIAL_RECEIVE_WITH_AUTHORIZATION_ENCRYPTED_TYPEHASH = 0x...;
// keccak256("CancelAuthorization(address authorizer,bytes32 nonce)")
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x...;
Functions
authorizationState
Returns the state of an authorization. Nonces are randomly generated 32-byte data unique to the authorizer's address.
function authorizationState(address authorizer, bytes32 nonce) external view returns (bool);
confidentialTransferWithAuthorization
Execute a transfer with a signed authorization using a plaintext amount (encrypted on-chain).
function confidentialTransferWithAuthorization(
address from,
address to,
uint64 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external returns (euint64 transferred);
confidentialReceiveWithAuthorization
Receive a transfer with a signed authorization. Has an additional check to ensure that the payee's address matches the caller to prevent front-running attacks.
function confidentialReceiveWithAuthorization(
address from,
address to,
uint64 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external returns (euint64 transferred);
confidentialTransferWithAuthorizationEncrypted
Execute a transfer with a signed authorization using a pre-encrypted amount.
function confidentialTransferWithAuthorizationEncrypted(
address from,
address to,
externalEuint64 encryptedAmount,
bytes calldata inputProof,
bytes32 encryptedAmountCommitment,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external returns (euint64 transferred);
confidentialReceiveWithAuthorizationEncrypted
Receive a transfer with a signed authorization using a pre-encrypted amount. Has an additional check to ensure that the payee's address matches the caller to prevent front-running attacks.
function confidentialReceiveWithAuthorizationEncrypted(
address from,
address to,
externalEuint64 encryptedAmount,
bytes calldata inputProof,
bytes32 encryptedAmountCommitment,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external returns (euint64 transferred);
cancelAuthorization
Attempt to cancel an authorization.
function cancelAuthorization(
address authorizer,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
Rationale
Two Authorization Variants
Plaintext amount: The authorizer signs a plaintext uint64 value. Simpler workflow when submitter address is unknown. Amount visible in calldata but encrypted on-chain.
Encrypted amount: The authorizer pre-encrypts the amount off-chain and signs over encryptedAmountCommitment = keccak256(abi.encode(encryptedAmount, inputProof)). Requires knowing the submitter address upfront (for fhEVM input binding). Better gas efficiency and full calldata privacy.
Encrypted Return Value
Unlike ERC-3009, transfers do not revert on insufficient balance (would leak balance information). Instead, transferred returns the actual amount moved (or zero). The submitter is granted decryption rights via FHE.allow() to verify success asynchronously.
uint64 vs uint256
ERC-7984 uses euint64 for balances (fhEVM constraint), so authorizations use uint64 instead of uint256.
Security Considerations
- Use
receiveWithAuthorizationvariants when calling from contracts to prevent front-running (same as ERC-3009) - Asynchronous verification: Submitters must decrypt the returned
transferredvalue off-chain to confirm success - Encrypted input binding: For encrypted variants, the
encryptedAmountis cryptographically bound to the submitter's address - No balance leak: Insufficient balance results in zero transfer without revert
Backwards Compatibility
This specification requires ERC-7984 as the base token. It is not backwards compatible with ERC-20 or ERC-3009 due to encrypted types and different return semantics.