Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP-3009: "Transfer With Authorization" - ERC20 meta-transactions #3010

Closed
petejkim opened this issue Sep 30, 2020 · 24 comments
Closed

EIP-3009: "Transfer With Authorization" - ERC20 meta-transactions #3010

petejkim opened this issue Sep 30, 2020 · 24 comments
Labels

Comments

@petejkim
Copy link
Contributor

petejkim commented Sep 30, 2020

Pull Request: EIP-3009: transferWithAuthorization

This is the place to discuss EIP-3009 "Transfer With Authorization", implemented by the USD Coin (USDC) v2 smart contract alongside EIP-2612: permit.



eip: 3009
title: Transfer With Authorization
author: Peter Jihoon Kim (@petejkim), Kevin Britz (@kbrizzle), David Knott (@DavidLKnott)
discussions-to: #3010
status: Draft
type: Standards Track
category: ERC
created: 2020-09-28
requires: 20, 712

Simple Summary

A contract interface that enables transferring of fungible assets via a signed authorization.

Abstract

A set of functions to enable meta-transactions and atomic interactions with ERC-20 token contracts via signatures conforming to the EIP-712 typed message signing specification.

This enables the user to:

  • delegate the gas payment to someone else,
  • pay for gas in the token itself rather than in ETH,
  • perform one or more token transfers and other operations in a single atomic transaction,
  • transfer ERC-20 tokens to another address, and have the recipient submit the transaction,
  • batch multiple transactions with minimal overhead, and
  • create and perform multiple transactions without having to worry about them failing due to accidental nonce-reuse or improper ordering by the miner.

Motivation

There is an existing spec, EIP-2612, that also allows meta-transactions, and it is encouraged that a contract implements both for maximum compatibility. The two primary differences between this spec and EIP-2612 are that:

  • EIP-2612 uses sequential nonces, but this uses random 32-byte nonces, and that
  • EIP-2612 relies on the ERC-20 approve/transferFrom ("ERC-20 allowance") pattern.

The biggest issue with the use of sequential nonces is that it does not allow users to perform more than one transaction at time without risking their transactions failing, because:

  • DApps may unintentionally reuse nonces that have not yet been processed in the blockchain.
  • Miners may process the transactions in the incorrect order.

This can be especially problematic if the gas prices are very high and transactions often get queued up and remain unconfirmed for a long time. Non-sequential nonces allow users to create as many transactions as they want at the same time.

The ERC-20 allowance mechanism is susceptible to the multiple withdrawal attack/SWC-114, and encourages antipatterns such as the use of the "infinite" allowance. The wide-prevalence of upgradeable contracts have made the conditions favorable for these attacks to happen in the wild.

The deficiencies of the ERC-20 allowance pattern brought about the development of alternative token standards such as the ERC-777 and ERC-677. However, they haven't been able to gain much adoption due to compatibility and potential security issues.

Specification

Event

event AuthorizationUsed(
    address indexed authorizer,
    bytes32 indexed nonce
);

// keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;

// keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;

/**
 * @notice Returns the state of an authorization
 * @dev Nonces are randomly generated 32-byte data unique to the authorizer's
 * address
 * @param authorizer    Authorizer's address
 * @param nonce         Nonce of the authorization
 * @return True if the nonce is used
 */
function authorizationState(
    address authorizer,
    bytes32 nonce
) external view returns (bool);

/**
 * @notice Execute a transfer with a signed authorization
 * @param from          Payer's address (Authorizer)
 * @param to            Payee's address
 * @param value         Amount to be transferred
 * @param validAfter    The time after which this is valid (unix time)
 * @param validBefore   The time before which this is valid (unix time)
 * @param nonce         Unique nonce
 * @param v             v of the signature
 * @param r             r of the signature
 * @param s             s of the signature
 */
function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external;

/**
 * @notice Receive a transfer with a signed authorization from the payer
 * @dev This has an additional check to ensure that the payee's address matches
 * the caller of this function to prevent front-running attacks. (See security
 * considerations)
 * @param from          Payer's address (Authorizer)
 * @param to            Payee's address
 * @param value         Amount to be transferred
 * @param validAfter    The time after which this is valid (unix time)
 * @param validBefore   The time before which this is valid (unix time)
 * @param nonce         Unique nonce
 * @param v             v of the signature
 * @param r             r of the signature
 * @param s             s of the signature
 */
function receiveWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external;

Optional:

event AuthorizationCanceled(
    address indexed authorizer,
    bytes32 indexed nonce
);

// keccak256("CancelAuthorization(address authorizer,bytes32 nonce)")
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429;

/**
 * @notice Attempt to cancel an authorization
 * @param authorizer    Authorizer's address
 * @param nonce         Nonce of the authorization
 * @param v             v of the signature
 * @param r             r of the signature
 * @param s             s of the signature
 */
function cancelAuthorization(
    address authorizer,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external;

The arguments v, r, and s must be obtained using the EIP-712 typed message signing spec.

Example:

DomainSeparator := Keccak256(ABIEncode(
  Keccak256(
    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
  ),
  Keccak256("USD Coin"),                      // name
  Keccak256("2"),                             // version
  1,                                          // chainId
  0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48  // verifyingContract
))

With the domain separator, the typehash, which is used to identify the type of the EIP-712 message being used, and the values of the parameters, you are able to derive a Keccak-256 hash digest which can then be signed using the token holder's private key.

Example:

// Transfer With Authorization
TypeHash := Keccak256(
  "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
)
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce }

// ReceiveWithAuthorization
TypeHash := Keccak256(
  "ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
)
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce }

// CancelAuthorization
TypeHash := Keccak256(
  "CancelAuthorization(address authorizer,bytes32 nonce)"
)
Params := { Authorizer, Nonce }
// "‖" denotes concatenation.
Digest := Keecak256(
  0x1901 ‖ DomainSeparator ‖ Keccak256(ABIEncode(TypeHash, Params...))
)

{ v, r, s } := Sign(Digest, PrivateKey)

Smart contract functions that wrap receiveWithAuthorization call may choose to reduce the number of arguments by accepting the full ABI-encoded set of arguments for the receiveWithAuthorization call as a single argument of the type bytes.

Example:

// keccak256("receiveWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)")[0:4]
bytes4 private constant _RECEIVE_WITH_AUTHORIZATION_SELECTOR = 0xef55bec6;

function deposit(address token, bytes calldata receiveAuthorization)
    external
    nonReentrant
{
    (address from, address to, uint256 amount) = abi.decode(
        receiveAuthorization[0:96],
        (address, address, uint256)
    );
    require(to == address(this), "Recipient is not this contract");

    (bool success, ) = token.call(
        abi.encodePacked(
            _RECEIVE_WITH_AUTHORIZATION_SELECTOR,
            receiveAuthorization
        )
    );
    require(success, "Failed to transfer tokens");

    ...
}

Use with web3 providers

The signature for an authorization can be obtained using a web3 provider with the eth_signTypedData{_v4} method.

Example:

const data = {
  types: {
    EIP712Domain: [
      { name: "name", type: "string" },
      { name: "version", type: "string" },
      { name: "chainId", type: "uint256" },
      { name: "verifyingContract", type: "address" },
    ],
    TransferWithAuthorization: [
      { name: "from", type: "address" },
      { name: "to", type: "address" },
      { name: "value", type: "uint256" },
      { name: "validAfter", type: "uint256" },
      { name: "validBefore", type: "uint256" },
      { name: "nonce", type: "bytes32" },
    ],
  },
  domain: {
    name: tokenName,
    version: tokenVersion,
    chainId: selectedChainId,
    verifyingContract: tokenAddress,
  },
  primaryType: "TransferWithAuthorization",
  message: {
    from: userAddress,
    to: recipientAddress,
    value: amountBN.toString(10),
    validAfter: 0,
    validBefore: Math.floor(Date.now() / 1000) + 3600, // Valid for an hour
    nonce: Web3.utils.randomHex(32),
  },
};

const signature = await ethereum.request({
  method: "eth_signTypedData_v4",
  params: [userAddress, JSON.stringify(data)],
});

const v = "0x" + signature.slice(130, 132);
const r = signature.slice(0, 66);
const s = "0x" + signature.slice(66, 130);

Rationale

Unique Random Nonce, Instead of Sequential Nonce

One might say transaction ordering is one reason why sequential nonces are preferred. However, sequential nonces do not actually help achieve transaction ordering for meta transactions in practice:

  • For native Ethereum transactions, when a transaction with a nonce value that is too-high is submitted to the network, it will stay pending until the transactions consuming the lower unused nonces are confirmed.
  • However, for meta-transactions, when a transaction containing a sequential nonce value that is too high is submitted, instead of staying pending, it will revert and fail immediately, resulting in wasted gas.
  • The fact that miners can also reorder transactions and include them in the block in the order they want (assuming each transaction was submitted to the network by different meta-transaction relayers) also makes it possible for the meta-transactions to fail even if the nonces used were correct. (e.g. User submits nonces 3, 4 and 5, but miner ends up including them in the block as 4,5,3, resulting in only 3 succeeding)
  • Lastly, when using different applications simultaneously, in absence of some sort of an off-chain nonce-tracker, it is not possible to determine what the correct next nonce value is if there exists nonces that are used but haven't been submitted and confirmed by the network.
  • Under high gas price conditions, transactions can often "get stuck" in the pool for a long time. Under such a situation, it is much more likely for the same nonce to be unintentionally reused twice. For example, if you make a meta-transaction that uses a sequential nonce from one app, and switch to another app to make another meta-transaction before the previous one confirms, the same nonce will be used if the app relies purely on the data available on-chain, resulting in one of the transactions failing.
  • In conclusion, the only way to guarantee transaction ordering is for relayers to submit transactions one at a time, waiting for confirmation between each submission (and the order in which they should be submitted can be part of some off-chain metadata), rendering sequential nonce irrelevant.

Valid After and Valid Before

  • Relying on relayers to submit transactions for you means you may not have exact control over the timing of transaction submission.
  • These parameters allow the user to schedule a transaction to be only valid in the future or before a specific deadline, protecting the user from potential undesirable effects that may be caused by the submission being made either too late or too early.

EIP-712

  • EIP-712 ensures that the signatures generated are valid only for this specific instance of the token contract and cannot be replayed on a different network with a different chain ID.
  • This is achieved by incorporating the contract address and the chain ID in a Keccak-256 hash digest called the domain separator. The actual set of parameters used to derive the domain separator is up to the implementing contract, but it is highly recommended that the fields verifyingContract and chainId are included.

Backwards Compatibility

New contracts benefit from being able to directly utilize EIP-3009 in order to create atomic transactions, but existing contracts may still rely on the conventional ERC-20 allowance pattern (approve/transferFrom).

In order to add support for EIP-3009 to existing contracts ("parent contract") that use the ERC-20 allowance pattern, a forwarding contract ("forwarder") can be constructed that takes an authorization and does the following:

  1. Extract the user and deposit amount from the authorization
  2. Call receiveWithAuthorization to transfer specified funds from the user to the forwarder
  3. Approve the parent contract to spend funds from the forwarder
  4. Call the method on the parent contract that spends the allowance set from the forwarder
  5. Transfer the ownership of any resulting tokens back to the user

Example:

interface IDeFiToken {
    function deposit(uint256 amount) external returns (uint256);

    function transfer(address account, uint256 amount)
        external
        returns (bool);
}

contract DepositForwarder {
    bytes4 private constant _RECEIVE_WITH_AUTHORIZATION_SELECTOR = 0xef55bec6;

    IDeFiToken private _parent;
    IERC20 private _token;

    constructor(IDeFiToken parent, IERC20 token) public {
        _parent = parent;
        _token = token;
    }

    function deposit(bytes calldata receiveAuthorization)
        external
        nonReentrant
        returns (uint256)
    {
        (address from, address to, uint256 amount) = abi.decode(
            receiveAuthorization[0:96],
            (address, address, uint256)
        );
        require(to == address(this), "Recipient is not this contract");

        (bool success, ) = address(_token).call(
            abi.encodePacked(
                _RECEIVE_WITH_AUTHORIZATION_SELECTOR,
                receiveAuthorization
            )
        );
        require(success, "Failed to transfer to the forwarder");

        require(
            _token.approve(address(_parent), amount),
            "Failed to set the allowance"
        );

        uint256 tokensMinted = _parent.deposit(amount);
        require(
            _parent.transfer(from, tokensMinted),
            "Failed to transfer the minted tokens"
        );

        uint256 remainder = _token.balanceOf(address(this);
        if (remainder > 0) {
            require(
                _token.transfer(from, remainder),
                "Failed to refund the remainder"
            );
        }

        return tokensMinted;
    }
}

Test Cases

See EIP3009.test.ts.

Implementation

EIP3009.sol

abstract contract EIP3009 is IERC20Transfer, EIP712Domain {
    // keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
    bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;

    // keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
    bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;

    mapping(address => mapping(bytes32 => bool)) internal _authorizationStates;

    event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);

    string internal constant _INVALID_SIGNATURE_ERROR = "EIP3009: invalid signature";

    function authorizationState(address authorizer, bytes32 nonce)
        external
        view
        returns (bool)
    {
        return _authorizationStates[authorizer][nonce];
    }

    function transferWithAuthorization(
        address from,
        address to,
        uint256 value,
        uint256 validAfter,
        uint256 validBefore,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        require(now > validAfter, "EIP3009: authorization is not yet valid");
        require(now < validBefore, "EIP3009: authorization is expired");
        require(
            !_authorizationStates[from][nonce],
            "EIP3009: authorization is used"
        );

        bytes memory data = abi.encode(
            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
            from,
            to,
            value,
            validAfter,
            validBefore,
            nonce
        );
        require(
            EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == from,
            "EIP3009: invalid signature"
        );

        _authorizationStates[from][nonce] = true;
        emit AuthorizationUsed(from, nonce);

        _transfer(from, to, value);
    }
}

IERC20Transfer.sol

abstract contract IERC20Transfer {
    function _transfer(
        address sender,
        address recipient,
        uint256 amount
    ) internal virtual;
}

EIP712Domain.sol

abstract contract EIP712Domain {
    bytes32 public DOMAIN_SEPARATOR;
}

EIP712.sol

library EIP712 {
    // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
    bytes32 public constant EIP712_DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;

    function makeDomainSeparator(string memory name, string memory version)
        internal
        view
        returns (bytes32)
    {
        uint256 chainId;
        assembly {
            chainId := chainid()
        }

        return
            keccak256(
                abi.encode(
                    EIP712_DOMAIN_TYPEHASH,
                    keccak256(bytes(name)),
                    keccak256(bytes(version)),
                    address(this),
                    bytes32(chainId)
                )
            );
    }

    function recover(
        bytes32 domainSeparator,
        uint8 v,
        bytes32 r,
        bytes32 s,
        bytes memory typeHashAndData
    ) internal pure returns (address) {
        bytes32 digest = keccak256(
            abi.encodePacked(
                "\x19\x01",
                domainSeparator,
                keccak256(typeHashAndData)
            )
        );
        address recovered = ecrecover(digest, v, r, s);
        require(recovered != address(0), "EIP712: invalid signature");
        return recovered;
    }
}

A fully working implementation of EIP-3009 can be found in this repository. The repository also includes an implementation of EIP-2612 that uses the EIP-712 library code presented above.

Security Considerations

Use receiveWithAuthorization instead of transferWithAuthorization when calling from other smart contracts. It is possible for an attacker watching the transaction pool to extract the transfer authorization and front-run the transferWithAuthorization call to execute the transfer without invoking the wrapper function. This could potentially result in unprocessed, locked up deposits. receiveWithAuthorization prevents this by performing an additional check that ensures that the caller is the payee. Additionally, if there are multiple contract functions accepting receive authorizations, the app developer could dedicate some leading bytes of the nonce could as the identifier to prevent cross-use.

When submitting multiple transfers simultaneously, be mindful of the fact that relayers and miners will decide the order in which they are processed. This is generally not a problem if the transactions are not dependent on each other, but for transactions that are highly dependent on each other, it is recommended that the signed authorizations are submitted one at a time.

The zero address must be rejected when using ecrecover to prevent unauthorized transfers and approvals of funds from the zero address. The built-in ecrecover returns the zero address when a malformed signature is provided.

Copyright

Copyright and related rights waived via CC0.

@petejkim petejkim changed the title EIP-3009 transferWithAuthorization: Gas-Abstracted ERC20 transactions EIP-3009 transferWithAuthorization: Gas-Abstracted ERC20 transactions (from USDC v2) Sep 30, 2020
@petejkim petejkim changed the title EIP-3009 transferWithAuthorization: Gas-Abstracted ERC20 transactions (from USDC v2) EIP-3009: "Transfer With Authorization" - ERC20 meta-transactions Sep 30, 2020
@petejkim
Copy link
Contributor Author

petejkim commented Sep 30, 2020

If you want to try it out, there is a fully gasless (free) demo that runs on Görli testnet: USDC L2 Demo. Click on "Give Me Some" to get tokens, again, gaslessly.

@MicahZoltu
Copy link
Contributor

Consider adding a method for invalidateNonce. This would allow someone to send a transaction that would just invalidate a nonce, without taking any action.

@juli
Copy link
Contributor

juli commented Oct 1, 2020

@MicahZoltu what is the benefit of that method? looks like it will make it easier to attack relayers.

@petejkim Glad to see more work towards standardization of token contract methods for gas less transactions. Unfortunatelly random nonces require extra storage. Sequential nonces to make sure calls are executed in order or not executed are a good feature IMO. I understand random nonces help relayers if transactions are mined out of order. But I don't think it is worth solving the sequential nonce reuse problem from the end users side this way. The user wallet could track what sequential nonces were used to avoid reusing them. If the users sign the same nonce on purpose, making relayers waste money then they can be punished.

@MicahZoltu
Copy link
Contributor

what is the benefit of that method? looks like it will make it easier to attack relayers

As a user, if I send a transaction out to take some action and for whatever reason it doesn't get picked up soon (perhaps I don't compensate the relayer enough in the transaction), I will forever have that particular transaction pending and available to be executed at any time in the future, potentially when it is profitable for someone else to have that transaction show up.

As a user, I want the ability to "cancel" that transaction by invalidating a particular nonce so that no one can include it at an arbitrary point in time in the future.

@petejkim
Copy link
Contributor Author

petejkim commented Oct 3, 2020

@MicahZoltu I agree with what you said. USDCv2 does have cancelAuthorization that does exactly that, but it was removed to keep this spec small, since it's possible to replicate that by calling transferWithAuthorization with 0 value and the same nonce. cancelAuthorization does take fewer arguments and consume less gas though. Do you think I should include it?

I will also address the rest of your feedback over the weekend, thanks for reviewing, really appreciate it.

@MicahZoltu
Copy link
Contributor

Keeping EIPs small and modular is certainly reasonable and I support it. Perhaps consider creating a second EIP that depends on this one which adds cancelAuthorization? Then implementors can choose to implement one or both. The advantage of an explicit cancel is the gas savings.

@yahgwai
Copy link

yahgwai commented Oct 5, 2020

There are other options for concurrent nonce management that may be relevant here. They typically require a bit more work on the client side, but can reduce gas costs in the contract by not having to always use a new storage slot.

Multinonce: A two dimensional nonce that allows the client to choose between concurrent and sequential behaviour. The nonce is both an index and a sequential nonce; the contract stores a mapping of index -> nonce. The client specifies both index and a nonce which is current nonce at that index + 1. For concurrency they use a new index, or a very old index for which they know no pending transactions exist. For 2 transactions that must be mined sequentially the client specifies the same index.

BitFlip: The nonce is a single bit to be turned on. If the bit is already on that nonce has already been used.

@juli
Copy link
Contributor

juli commented Oct 9, 2020

I suggest using mapping (address => mapping(address => uint256)) nonces to have sequential nonces for each account holder and each different tx.origin (EOAs used by relayers) . This way users can't invalidate relayed transactions without the relayer accepting the new version of the transaction with an already used nonce. This approach is aligned with relayer's incentives. This also solves the out of order mining problem, makes it a bit harder to attack relayers, and storage cost is reduced.

@sohkai
Copy link

sohkai commented Oct 12, 2020

Edit: Ah, I just saw this was also mentioned on the original PR as a comment to remove the internal state details as well.

Ccould we simplify the requirement of a separate function name by simply using the auto-generated getter (as EIP-2612 also currently suggests)?

So instead of:

mapping(address => mapping(bytes32 => bool)) authorizationStates;

function authorizationState(
    address authorizer,
    bytes32 nonce
) external view returns (bool);

It's just:

mapping(address => mapping(bytes32 => bool)) public authorizationState;

@petejkim
Copy link
Contributor Author

@sohkai Yep, that works!

@invocamanman
Copy link

I think might be helpful add a security consideration to never use the transferWithAuthorization as a method of the token reception protocol of any smart contract, in other words, use it as a approve/transferFrom in the same transaction.
The reason behind is that the signature of the transferWithAuthorization could be taken and used, so anyone could perform a frontRunning attack, stealing the signature and executing transferWithAuthorization directly to the smart contract, skipping the reception protocol and likely result in the loss of the tokens.

An example of the attack:
Suppose a case that User A wants to deposit 1000 tokens to the Smart Contract B. Smart contract B has a deposit function wich implements transferWithAuthorization as a token reception method. When User A sends the transaction, including the signature to trigger the transferWithAuthorization, someone could perform the attack previously described, the 1000 tokens will be correctly transferred from the user to the smart contract, but the smart contract would not be aware, and the user has lost his tokens

@leekt
Copy link
Contributor

leekt commented Oct 19, 2020

Seems it needs some msg.sender verification to reduce the front-running risk @invocamanman described.
like require(msg.sender == _to) or get _msgSender as parameter and verify that within signature and require(msg.sender == _msgSender)

@petejkim
Copy link
Contributor Author

petejkim commented Oct 20, 2020

I think it's useful to support being able to send to EOAs, so I think there are two ways to address this:

  1. If the recipient is a contract address (extcodesize > 0), require the recipient address (to) to be msg.sender.
    OR
  2. Add another type hash ReceiveWithAuthorization, which requires the recipient address to be msg.sender.

@petejkim
Copy link
Contributor Author

petejkim commented Nov 6, 2020

There are other options for concurrent nonce management that may be relevant here. They typically require a bit more work on the client side, but can reduce gas costs in the contract by not having to always use a new storage slot.

Multinonce: A two dimensional nonce that allows the client to choose between concurrent and sequential behaviour. The nonce is both an index and a sequential nonce; the contract stores a mapping of index -> nonce. The client specifies both index and a nonce which is current nonce at that index + 1. For concurrency they use a new index, or a very old index for which they know no pending transactions exist. For 2 transactions that must be mined sequentially the client specifies the same index.

BitFlip: The nonce is a single bit to be turned on. If the bit is already on that nonce has already been used.

Those are interesting ideas, but in absence of some off-chain nonce tracker, it doesn't solve the problem of not being able to reliably obtain an unused nonce, especially when multiple DApps are used simultaneously. Bitflip is certainly better than the conventional incremental nonce though.

@yahgwai
Copy link

yahgwai commented Nov 9, 2020

it doesn't solve the problem of not being able to reliably obtain an unused nonce, especially when multiple DApps are used simultaneously

Dapps can do this by choosing a random index - the collision chance is negligible. Ofc each new dapp wold incur a 20k store instead of a 5k store for using a new rand.

Other options for figuring out the currently used nonces are to ask the wallet for the next nonce, or to look in the pending pool for the AuthorizationUsed event being emitted from transactions.

Generally though I think @juli's mapping (address => mapping(address => uint256)) nonces may be the best approach

@ThomasRogg
Copy link

A side note: I believe that there is a bug in the code listed in the EIP-3009. I fixed it in a pull request on the repo the actual code files are in: CoinbaseStablecoin/eip-3009#1

The USDCv2 code seems to be correct, however. Just not the reference implementation I see in the EIP-3009.

@github-actions
Copy link

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

@github-actions github-actions bot added stale and removed stale labels Oct 24, 2021
@ethereum ethereum deleted a comment from VallonV89 Dec 22, 2021
@ethereum ethereum deleted a comment from bisoks Dec 22, 2021
@github-actions
Copy link

github-actions bot commented Jan 5, 2022

This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.

@github-actions github-actions bot closed this as completed Jan 5, 2022
@omidziaee
Copy link

Hi,
Quick question regarding the "Use with web3 providers" section, after you get the v, r, s how do you submit the transaction? Do you use the contract method "transferWithAuthorization"? Could you please elaborate more on that with an example?
Thanks,

@jejopl
Copy link

jejopl commented Jan 28, 2022

@omidziaee Yeah, you are calling contract method. Here's the example in JS

const ABI = [...];
const tokenAddress = "0x213..."
const YourToken = new window.web3.eth.Contract(ABI, tokenAddress);

// transaction that you have signed
const transactionData = {
   owner: ...
   [...]
}

const owner =  transactionData.owner
const spender = transactionData.spender
const amount = web3.utils.toWei(transactionData.amount, "Ether");
const validAfter = transactionData.valid_after;
const validBefore = transactionData.valid_before;
const nonce = transactionData.nonce;
const { v, r, s } = transactionData.signature;

const transaction = await YourToken.methods
    .transferWithAuthorization(owner, spender, amount, validAfter, validBefore, nonce, v, r, s)
    .send({ from: spender, gas: "21000" })
    .catch((error) => ({error}));
    
 if(transaction.error) {
 // handle error here
 } else if (transaction.transactionHash) {
 // success
 }

@danielnordh
Copy link

danielnordh commented Feb 8, 2022

I'm trying to use transferWithAuthorization with a Dart client approving the transaction, then sending it to a JS backend server for submitting to the blockchain.

I'm using the eth-sig-util package for signing the typed data, and the web3dart package for creating and signing the transaction.

My understanding of the flow so far

  1. Create typedData
  2. Sign typedData
  3. Create transaction calling transferWithAuthorization
  4. Sign transaction
  5. Send signed transaction to backend
  6. Have backend submit the signed transaction to the blockchain

I'm currently getting stuck on step 4, with Web3.Transaction.callContract complaining about a wrong type but not saying which parameter it refers to. Error: int is not a subtype of BigInt

The only parameter that seems to be an int is v.

Update
Casting the v, r, s parameters to the following gets me past step 3.

BigInt.from(sigParams.v),
bigIntToUint8List(sigParams.r),
bigIntToUint8List(sigParams.s)

The latter two done with:

Uint8List bigIntToUint8List(BigInt bigInt) =>
    bigIntToByteData(bigInt).buffer.asUint8List();

ByteData bigIntToByteData(BigInt bigInt) {
  final data = ByteData((bigInt.bitLength / 8).ceil());
  var _bigInt = bigInt;

  for (var i = 1; i <= data.lengthInBytes; i++) {
    data.setUint8(data.lengthInBytes - i, _bigInt.toUnsigned(8).toInt());
    _bigInt = _bigInt >> 8;
  }
  return data;
}

But now I get stuck on step 4, ethClient.signTransaction, with an invalid v value:
RPCError (RPCError: got code 3 with msg "execution reverted: ECRecover: invalid signature 'v' value".)

Dart code for a function that should return a hash of the broadcast transaction:

Future<String?> signUSDC(BigInt amount, Web3Dart.EthereumAddress ethAddress, User user) async {
    final config = ref.read(configurationProvider).instance;
    final wallet =
        Web3Dart.Wallet.fromJson(user.ethereumWallet!, kWalletPassword);

    final validAfter = BigInt.zero;
    final validBefore = BigInt.from(9223372036854775807);
    final nonce = Nonce.secure(32);
    final nonceBytes = utf8.encode(nonce.toString());

    final Map<String, dynamic> rawTypedData = {
      "types": {
        "EIP712Domain": [
          {"name": "name", "type": "string"},
          {"name": "version", "type": "string"},
          {"name": "chainId", "type": "uint256"},
          {"name": "verifyingContract", "type": "address"}
        ],
        "TransferWithAuthorization": [
          {"name": "from", "type": "address"},
          {"name": "to", "type": "address"},
          {"name": "value", "type": "uint256"},
          {"name": "validAfter", "type": "uint256"},
          {"name": "validBefore", "type": "uint256"},
          {"name": "nonce", "type": "bytes32"}
        ]
      },
      "domain": {
        "name": "USDC",
        "version": "2",
        "chainId": config.chain.chainId,
        "verifyingContract": config.chain.usdcContract
      },
      "primaryType": "TransferWithAuthorization",
      "message": {
        "from": user.pubKey,
        "to": ethAddress.toString(),
        "value": amount.toString(),
        "validAfter": validAfter.toString(),
        "validBefore": validBefore.toString(),
        "nonce": nonceBytes.toString()
      }
    };

    String signature = EthSigUtil.signTypedData(
        privateKey: bytesToHex(wallet.privateKey.privateKey),
        jsonData: jsonEncode(rawTypedData),
        version: TypedDataVersion.V4,
        chainId: config.chain.chainId);

    final contract = await ref.read(usdcContractProvider.future);
    var sigParams = SignatureUtil.fromRpcSig(signature);

    final transaction = Web3Dart.Transaction.callContract(
      contract: contract,
      function: contract.function('transferWithAuthorization'),
      parameters: [
        Web3Dart.EthereumAddress.fromHex(user.pubKey!),
        ethAddress,
        amount,
        validAfter,
        validBefore,
        nonceBytes,
        BigInt.from(sigParams.v),
        bigIntToUint8List(sigParams.r),
        bigIntToUint8List(sigParams.s)
      ],
    );
    final signedTx = await ethClient.signTransaction(wallet.privateKey, transaction);
    final hexTx = "0x" + hex.encode(signedTx); // not sure if signedTx or hexTx should be used

    try {
      // submit signed transaction to backend, get hash in return
      final body = json.encode((await defaultParams)
        ..addAll({
          "serializedTx": signedTx,
          "validationCode": "redacted_code"
        }));

      return sendRequest(baseUrl.asUri / 'redacted_endpoint',
          method: DataRequestMethod.POST,
          body: body,
          omitDefaultParams: false, onSuccess: (map) {
        return map['hash'] as String;
      }, onError: (error) {
        return null;
      });
    } catch (error) {
      return null;
    }
  }

@grantmike
Copy link

I have an EIP which adds 13 lines of functionality to this EIP, would love your thoughts 😄 (from Circle engineering team!) #5987

@sohkai @invocamanman @leekt @ThomasRogg @danielnordh @omidziaee @jejopl @kbrizzle @DavidLKnott @juli @MiCh @yahgwai

@masehdh
Copy link

masehdh commented Mar 5, 2023

@danielnordh did you end up finding a way to get past step 4?

@Pandapip1
Copy link
Member

Locking this issue, as it is no longer the discussion link.

@ethereum ethereum locked and limited conversation to collaborators Mar 5, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

16 participants