EIP: Contract signature validation extension for ERC-3009 Transfer with Authorization

Abstract

This proposal aims to extend the functionality of the existing ERC-3009 standard, “Transfer With Authorization,” to support transfer operations initiated by smart contract wallets.

Motivation

The existing ERC-3009 standard enables asset transfers with ECDSA signatures. However, as smart contract wallets become more prevalent in the ecosystem, the current standard is no longer sufficient.

This proposal aims to enhance the usability and composeability of the standard by extending ERC-3009 with smart contract wallet signature validation, as defined in ERC-1271. By incorporating this extension, users will have greater flexibility in managing their assets while ensuring a secure authorization process.

Specification

The following events and interfaces must still be present given the initial spec defined in ERC-3009.

  • Event AuthorizationUsed.
  • Constants TRANSFER_WITH_AUTHORIZATION_TYPEHASH and RECEIVE_WITH_AUTHORIZATION_TYPEHASH.
  • View function interface authorizationState(address authorizer, bytes32 nonce)

In addition, the following interfaces must be added to be compliant with the standard:

/**
 * @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 signature     Unstructured bytes signature signed by an EOA wallet or a contract wallet
 */
function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    bytes memory signature
) 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 signature     Unstructured bytes signature signed by an EOA wallet or a contract wallet
 */
function receiveWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    bytes memory signature
) external;

Optional:

The AuthorizationCanceled event and CANCEL_AUTHORIZATION_TYPEHASH constant as defined in the ERC-3009 spec.

/**
 * @notice Attempt to cancel an authorization
 * @param authorizer    Authorizer's address
 * @param nonce         Nonce of the authorization
 * @param signature     Unstructured bytes signature signed by an EOA wallet or a contract wallet
 */
function cancelAuthorization(
    address authorizer,
    bytes32 nonce,
    bytes memory signature
) external;

Rationale

By replacing the existing V, R, S signature validation scheme and introducing support for unstructured bytes input, contract developers can use a unified interface to validate signature from both EOAs and SC wallets. This allows for the utilization of different signature schemes and algorithms fitting the wallet type, paving the way for smart contract wallets and advanced wallet types to enhance their signature validation processes, promoting flexibility and innovation.

Backwards Compatibility

This proposal is fully backward-compatible with the existing ERC-3009 standard. Contracts that currently rely on the V, R, S signature validation scheme will continue to function without any issues.

In the event that both the existing V, R, S signature validation scheme and the new unstructured bytes signature validation need to be supported for backward compatibility, developers can reduce duplicates by adapting the following code block as an example:

function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    transferWithAuthorization(owner, spender, value, deadline, abi.encodePacked(r, s, v));
}

Reference Implementation

/**
  * @notice Execute a transfer with a signed authorization
  * @dev EOA wallet signatures should be packed in the order of r, s, v.
  * @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 signature     Signature byte array produced by an EOA wallet or a contract wallet
  */
function _transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    bytes memory signature
) internal {
    require(now > validAfter, "Authorization is not yet valid");
    require(now < validBefore, "Authorization is expired");
    require(!_authorizationStates[authorizer][nonce], "Authorization is used or canceled");

    bytes32 digest = keccak256(abi.encodePacked(
        hex"1901",
        DOMAIN_SEPARATOR,
        keccak256(abi.encode(
            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
            from,
            to,
            value,
            validAfter,
            validBefore,
            nonce
        ))
    ));
    require(
        // Check for both ECDSA signature and and ERC-1271 signature. A sample SignatureChecker is available at
        // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7bd2b2a/contracts/utils/cryptography/SignatureChecker.sol
        SignatureChecker.isValidSignatureNow(
            owner,
            typedDataHash,
            signature
        ),
        "Invalid signature"
    );

    _authorizationStates[authorizer][nonce] = true;
    emit AuthorizationUsed(authorizer, nonce);
    
    _transfer(from, to, value);
}

Security Considerations

  • For contract wallets, the security of transferWithAuthorization, receiveWithAuthorization, and cancelAuthorization rely on ContractWallet.isValidSignature() to ensure the signature bytes represent the desired execution from contract wallet owner(s). Contract wallet developers must exercise caution when implementing custom signature validation logic to ensure the security of their contracts.

Copyright and related rights waived via CC0.