Source code for eth_defi.usdc.eip_3009

"""EIP-3009 transferWithAuthorization() support for Python.

- Support `transferWithAuthorization()` and `receiveWithAuthorization()` ERC-20 single-click token transfers

- `EIP-3009 spec <https://github.com/ethereum/EIPs/issues/3010>`__.

- `JavaScript example <https://github.com/ZooWallet/safe-contracts/blob/b2abebd0fdd7f8a846dfc2d59233e41487b659cf/scripts/usdc-biconomy-transferWithAuthorization.js#L87>`__.

- `Canonical examples to construct USDC EIP-712 messages <https://github.com/centrehq/centre-tokens/blob/master/test/v2/GasAbstraction/helpers.ts>`__.

- See `USDC payment forwarder for Enzyme protocol as an example contract <https://github.com/tradingstrategy-ai/web3-ethereum-defi/blob/master/contracts/in-house/src/VaultUSDCPaymentForwarder.sol>`__

- `See how to deploy the payment forwarder contract <https://github.com/tradingstrategy-ai/web3-ethereum-defi/tree/master/contracts/in-house>`__

Internally we use :py:mod:`eth_defi.eip_712` module for managing the messages here.

.. warning::

    Polygon bridged tokens' `transferWithAuthorization()` is not compatible
    with EIP-3009. This includes *USD Coin (PoS)*, or USDC on Polygon.
    See :py:func:`make_eip_3009_transfer` for workarounds.

"""
import datetime
import enum
import secrets
import warnings

from eth_account._utils.signing import to_bytes32
from eth_account.signers.local import LocalAccount
from web3.contract.contract import ContractFunction

from eth_defi.eip_712 import eip712_encode_hash
from eth_defi.token import TokenDetails
from eth_typing import HexAddress


[docs]class EIP3009AuthorizationType(enum.Enum): """EIP-3009 message types. - Note that some contracts e.g. Polygon's bridged USDC only implement `TransferWithAuthorization` support. Always check your target token first for supported methods. - Use `ReceiveWithAuthorization` when possible as it is safer due to front running protection See :py:func:`make_eip_3009_transfer` for more information. """ #: Used with USDC TransferWithAuthorization = "TransferWithAuthorization" #: Used with USDC #: #: Not avaible on Polygon bridged tokens #: ReceiveWithAuthorization = "ReceiveWithAuthorization"
[docs]def construct_eip_3009_authorization_message( chain_id: int, token: TokenDetails, from_, to, value, valid_before=0, valid_after=1, duration_seconds=0, authorization_type=EIP3009AuthorizationType.TransferWithAuthorization, ) -> dict: """Create EIP-712 message for EIP-3009 transfers. - Used to construct the message that then needs to be signed - The signature will be verified by `receiveWithAuthorization()` or `transferWithAuthorization` function in the token contract .. note :: For Polygon USDC `transferWithAuthorization()` you need to use a different message structure, because Polygon bridged tokens are not EIP-3009 compatible. This function cannot work with Polygon bridged tokens. Polygon version is: .. code-block:: text EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'verifyingContract', type: 'address' }, { name: 'salt', type: 'bytes32' } ] More on Polygon incompatibility - `Ethers.js disucssion <https://github.com/ethers-io/ethers.js/discussions/2886>`__ - `Stackexchange discussion <https://ethereum.stackexchange.com/q/141968/620>`__. :return: JSON message for EIP-712 signing. """ assert duration_seconds or valid_before, "You need to give either duration_seconds or valid_before" # Relative to the current time if duration_seconds: assert not valid_before, "You cannot give valid_before with duration_seconds" assert duration_seconds > 0 valid_before = int(datetime.datetime.utcnow().timestamp() + duration_seconds) data = { "types": { "EIP712Domain": [ {"name": "name", "type": "string"}, {"name": "version", "type": "string"}, {"name": "chainId", "type": "uint256"}, {"name": "verifyingContract", "type": "address"}, ], authorization_type.value: [ {"name": "from", "type": "address"}, {"name": "to", "type": "address"}, {"name": "value", "type": "uint256"}, {"name": "validAfter", "type": "uint256"}, {"name": "validBefore", "type": "uint256"}, {"name": "nonce", "type": "bytes32"}, ], }, # domainSeparator = makeDomainSeparator( # "USD Coin", # "2", # 1, // hardcoded to 1 because of ganache bug: https://github.com/trufflesuite/ganache/issues/1643 # getFiatToken().address # ); "domain": { "name": token.name, "version": "2", # TODO: Read from USDC contract? "chainId": chain_id, "verifyingContract": token.address, }, "primaryType": authorization_type.value, "message": {"from": from_, "to": to, "value": value, "validAfter": valid_after, "validBefore": valid_before, "nonce": secrets.token_bytes(32)}, # 256-bit random nonce } return data
[docs]def make_eip_3009_transfer( token: TokenDetails, from_: LocalAccount, to: HexAddress, func: ContractFunction, value: int, valid_before: int = 0, valid_after: int = 1, duration_seconds: int = 0, extra_args=(), authorization_type=EIP3009AuthorizationType.TransferWithAuthorization, ) -> ContractFunction: """Perform an EIP-3009 transferWithAuthorization() and receiveWithAuthorization() transaction. - Constructs the EIP-3009 / EIP-712 payload - Signs the message - Builds a transaction against `receiveWithAuthorization` in USDC using Web3.py API, assuming there is a target smart contract function `func` to be called with this messge - The caller can then execute this by building a transaction from the resulting bound function call .. note :: This currently supports only `LocalAccount` because of `missing features in web3.py <https://github.com/ethereum/web3.py/issues/2180#issuecomment-943590192>`__. Example: .. code-block:: python # Deploy some contract that supports payment_forwarder = deploy_contract( web3, "VaultUSDCPaymentForwarder.json", deployer, usdc.address, comptroller.address, ) # Construct bounded ContractFunction instance # that will transact with MockEIP3009Receiver.deposit() # smart contract function. bound_func = make_receive_with_authorization_transfer( token=usdc, from_=user, to=payment_forwarder.address, func=payment_forwarder.functions.buySharesOnBehalf, value=500 * 10**6, # 500 USD, valid_before=valid_before, extra_args=(1,), # minSharesQuantity ) # Sign and broadcast the tx tx_hash = bound_func.transact({"from": user.address}) The `receiveAuthorization()` signature is: .. code-block:: text function receiveWithAuthorization( address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) :param token: USDC token details :param from_: The local account that signs the EIP-3009 transfer. :param to: To which contract USDC is transferred. :param func: The contract function that is verifying the transfer. A smart contract function with the same call signature as receiveAuthorization(). However, the spec does not specify what kind of a signature of a function this is: you can transfer `receiveWithAuthorization()` payload in any form, with extra parameters, byte packed, etc. :param value: How many tokens in raw value :param valid_before: Transfer timeout control :param valid_after: Transfer timeout control :param duration_seconds: Automatically set valid_before based on this :param extra_args: Arguments added after the standard receiveWithAuthorization() prologue. :param authorization_type: Is this `transferWithAuthorization` or `receiveWithAuthorization` style transaction. :return: Bound contract function for transferWithAuthorization """ assert isinstance(token, TokenDetails) assert isinstance(func, ContractFunction) assert isinstance(from_, LocalAccount) assert to.startswith("0x") assert value > 0 web3 = token.contract.w3 chain_id = web3.eth.chain_id data = construct_eip_3009_authorization_message( chain_id=chain_id, token=token, from_=from_.address, to=to, value=value, valid_before=valid_before, valid_after=valid_after, duration_seconds=duration_seconds, authorization_type=authorization_type, ) # The message payload is receiveAuthorization arguments, tightly encoded, # without the function selector message_hash = eip712_encode_hash(data) # TODO: There is no public Web3.py method to sign raw hashes # Mute DeprecationWarning with warnings.catch_warnings(): warnings.filterwarnings(action="ignore", category=DeprecationWarning) signed_message = from_.signHash(message_hash) # Should come in the order defined for the dict, # as Python 3.10+ does ordered dicts args = list(data["message"].values()) # from, to, value, validAfter, validBefore, nonce args += [signed_message.v, to_bytes32(signed_message.r), to_bytes32(signed_message.s)] args += list(extra_args) return func(*args)