Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions contracts/interfaces/draft-IERC3009.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.4.16;

/**
* @dev Interface of the ERC-3009 standard as defined in https://eips.ethereum.org/EIPS/eip-3009[ERC-3009].
*/
interface IERC3009 {
/// @dev Emitted when an authorization is used.
event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);

/**
* @dev Returns the state of an authorization.
*
* Nonces are randomly generated 32-byte values unique to the authorizer's address.
*/
function authorizationState(address authorizer, bytes32 nonce) external view returns (bool);
Comment on lines +12 to +17
Copy link
Contributor

@gonzaotc gonzaotc Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we be a bit more specific in the comments here about the boolean returned value?

i.e, what does it mean the authorization state to be returned as true or false?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without a clear explanation of the returned boolean, true sounds to me like the authorization state is "positive", while it is actually the other way around (the nonce is invalid, has been consumed already)


/**
* @dev Executes a transfer with a signed authorization.
*
* Requirements:
*
* * `validAfter` must be less than the current block timestamp.
* * `validBefore` must be greater than the current block timestamp.
* * `nonce` must not have been used by the `from` account.
* * the signature must be valid for the authorization.
*/
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;

/**
* @dev Receives a transfer with a signed authorization from the payer.
*
* Includes an additional check to ensure that the payee's address (`to`) matches the caller
* to prevent front-running attacks.
*
* Requirements:
*
* * `to` must be the caller of this function.
* * `validAfter` must be less than the current block timestamp.
* * `validBefore` must be greater than the current block timestamp.
* * `nonce` must not have been used by the `from` account.
* * the signature must be valid for the authorization.
*/
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
}

/**
* @dev Extension of {IERC3009} that adds the ability to cancel authorizations.
*/
interface IERC3009Cancel {
/// @dev Emitted when an authorization is canceled.
event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce);

/**
* @dev Cancels an authorization.
*
* Requirements:
*
* * `nonce` must not have been used by the `authorizer` account.
* * the signature must be valid for the cancellation.
*/
function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external;
}
274 changes: 274 additions & 0 deletions contracts/token/ERC20/extensions/ERC20TransferAuthorization.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "../ERC20.sol";
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
import {SignatureChecker} from "../../../utils/cryptography/SignatureChecker.sol";
import {MessageHashUtils} from "../../../utils/cryptography/MessageHashUtils.sol";
import {IERC3009, IERC3009Cancel} from "../../../interfaces/draft-IERC3009.sol";

/**
* @dev Implementation of the ERC-3009 Transfer With Authorization extension allowing
* transfers to be made via signatures, as defined in https://eips.ethereum.org/EIPS/eip-3009[ERC-3009].
*
* Adds the {transferWithAuthorization} and {receiveWithAuthorization} methods, which
* can be used to change an account's ERC-20 balance by presenting a message signed
* by the account. By not relying on {IERC20-approve} and {IERC20-transferFrom}, the
* token holder account doesn't need to send a transaction, and thus is not required
* to hold native currency (e.g. ETH) at all.
*
* NOTE: This extension uses non-sequential nonces to allow for flexible transaction ordering
* and parallel transaction submission, unlike {ERC20Permit} which uses sequential nonces.
*/
abstract contract ERC20TransferAuthorization is ERC20, EIP712, IERC3009, IERC3009Cancel {
/// @dev The signature is invalid
error ERC3009InvalidSignature();

/// @dev The authorization is already used or canceled
error ERC3009ConsumedAuthorization(address authorizer, bytes32 nonce);

/// @dev The authorization is not valid at the given time
error ERC3009InvalidAuthorizationTime(uint256 validAfter, uint256 validBefore);

bytes32 private constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH =
keccak256(
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
bytes32 private constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH =
keccak256(
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
bytes32 private constant CANCEL_AUTHORIZATION_TYPEHASH =
keccak256("CancelAuthorization(address authorizer,bytes32 nonce)");

mapping(address => mapping(bytes32 => bool)) private _consumed;

/**
* @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
*
* It's a good idea to use the same `name` that is defined as the ERC-20 token name.
*/
constructor(string memory name) EIP712(name, "1") {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we ever do contract MyToken is ERC20Permit, ERC20TransferAuthorization {...} would this cause a conflict ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, it would cause

DeclarationError: Base constructor arguments given twice.
 --> contracts/mocks/token/ERC20TransferAuthorizationBlockNumberMock.sol:9:1:
  |
9 | abstract contract ERC20TransferAuthorizationBlockNumberMock is ERC20Permit, ERC20TransferAuthorization {
  | ^ (Relevant source part starts here and spans across multiple lines).
Note: First constructor call is here:
  --> contracts/token/ERC20/extensions/ERC20Permit.sol:39:37:
   |
39 |     constructor(string memory name) EIP712(name, "1") {}
   |                                     ^^^^^^^^^^^^^^^^^
Note: Second constructor call is here:
  --> contracts/token/ERC20/extensions/ERC20TransferAuthorization.sol:56:37:
   |
56 |     constructor(string memory name) EIP712(name, "1") {}
   |                                     ^^^^^^^^^^^^^^^^^

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solutions I see:

  • have ERC20TransferAuthorization inherit from ERC20Permit. That includes EIP712, and the corresponding constructor.
  • remove that constructor from ERC20TransferAuthorization
    • if the contract inherit from ERC20Permit and ERC20TransferAuthorization, then the EIP constructor from ERC20Permit would do the work
    • if the contract doesn't inherit from ERC20Permit, then the user will have to call EIP721 constructor itself.


/// @inheritdoc IERC3009
function authorizationState(address authorizer, bytes32 nonce) public view virtual returns (bool) {
return _consumed[authorizer][nonce];
}

/// @inheritdoc IERC3009
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
_transferWithAuthorization(from, to, value, validAfter, validBefore, nonce, v, r, s);
}

/// @dev Same as {transferWithAuthorization} but with a bytes signature.
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) public virtual {
_transferWithAuthorization(from, to, value, validAfter, validBefore, nonce, signature);
}

/// @inheritdoc IERC3009
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
_receiveWithAuthorization(from, to, value, validAfter, validBefore, nonce, v, r, s);
}

/// @dev Same as {receiveWithAuthorization} but with a bytes signature.
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) public virtual {
_receiveWithAuthorization(from, to, value, validAfter, validBefore, nonce, signature);
}

/// @inheritdoc IERC3009Cancel
function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) public virtual {
_cancelAuthorization(authorizer, nonce, v, r, s);
}

/// @dev Same as {cancelAuthorization} but with a bytes signature.
function cancelAuthorization(address authorizer, bytes32 nonce, bytes memory signature) public virtual {
_cancelAuthorization(authorizer, nonce, signature);
}

/**
* @dev Returns the domain separator used in the encoding of the signature for
* {transferWithAuthorization} and {receiveWithAuthorization}, as defined by {EIP712}.
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _domainSeparatorV4();
}

/// @dev Internal version of {transferWithAuthorization}.
function _transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) internal virtual {
_transferWithAuthorization(from, to, value, validAfter, validBefore, nonce, abi.encodePacked(r, s, v));
}

/// @dev Internal version of {transferWithAuthorization} that accepts a bytes signature.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_transferWithAuthorization, _receiveWithAuthorization and _cancelAuthorization NatSpec claims that they receive a bytes signature, while they don't

function _transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) internal virtual {
require(
block.timestamp > validAfter && block.timestamp < validBefore,
ERC3009InvalidAuthorizationTime(validAfter, validBefore)
);
require(!authorizationState(from, nonce), ERC3009ConsumedAuthorization(from, nonce));
require(
_validateTransferWithAuthorization(from, to, value, validAfter, validBefore, nonce, signature),
ERC3009InvalidSignature()
);

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

/**
* @dev Internal version of {receiveWithAuthorization}.
*
* Includes an additional check to ensure that the payee's address matches the caller to prevent
* front-running attacks.
*/
function _receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) internal virtual {
_receiveWithAuthorization(from, to, value, validAfter, validBefore, nonce, abi.encodePacked(r, s, v));
}

/// @dev Internal version of {receiveWithAuthorization} that accepts a bytes signature.
function _receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) internal virtual {
require(to == _msgSender(), ERC20InvalidReceiver(to));
require(
block.timestamp > validAfter && block.timestamp < validBefore,
ERC3009InvalidAuthorizationTime(validAfter, validBefore)
);
require(!authorizationState(from, nonce), ERC3009ConsumedAuthorization(from, nonce));
require(
_validateReceiveWithAuthorization(from, to, value, validAfter, validBefore, nonce, signature),
ERC3009InvalidSignature()
);

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

/// @dev Internal version of {cancelAuthorization}.
function _cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) internal virtual {
_cancelAuthorization(authorizer, nonce, abi.encodePacked(r, s, v));
}

/// @dev Internal version of {cancelAuthorization} that accepts a bytes signature.
function _cancelAuthorization(address authorizer, bytes32 nonce, bytes memory signature) internal virtual {
require(!authorizationState(authorizer, nonce), ERC3009ConsumedAuthorization(authorizer, nonce));
require(_validateCancelAuthorization(authorizer, nonce, signature), ERC3009InvalidSignature());

_consumed[authorizer][nonce] = true;
emit AuthorizationCanceled(authorizer, nonce);
}

/// @dev Validates the transfer with authorization signature.
function _validateTransferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) internal virtual returns (bool) {
bytes32 hash = _hashTypedDataV4(
keccak256(abi.encode(TRANSFER_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce))
);
return SignatureChecker.isValidSignatureNow(from, hash, signature);
}

/// @dev Validates the receive with authorization signature.
function _validateReceiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) internal virtual returns (bool) {
bytes32 hash = _hashTypedDataV4(
keccak256(abi.encode(RECEIVE_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce))
);
return SignatureChecker.isValidSignatureNow(from, hash, signature);
}

/// @dev Validates the cancel authorization signature.
function _validateCancelAuthorization(
address authorizer,
bytes32 nonce,
bytes memory signature
) internal virtual returns (bool) {
bytes32 hash = _hashTypedDataV4(keccak256(abi.encode(CANCEL_AUTHORIZATION_TYPEHASH, authorizer, nonce)));
return SignatureChecker.isValidSignatureNow(authorizer, hash, signature);
}
}
17 changes: 17 additions & 0 deletions test/helpers/eip712-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ module.exports = mapValues(
salt: 'bytes32',
},
Permit: { owner: 'address', spender: 'address', value: 'uint256', nonce: 'uint256', deadline: 'uint256' },
TransferWithAuthorization: {
from: 'address',
to: 'address',
value: 'uint256',
validAfter: 'uint256',
validBefore: 'uint256',
nonce: 'bytes32',
},
ReceiveWithAuthorization: {
from: 'address',
to: 'address',
value: 'uint256',
validAfter: 'uint256',
validBefore: 'uint256',
nonce: 'bytes32',
},
CancelAuthorization: { authorizer: 'address', nonce: 'bytes32' },
Ballot: { proposalId: 'uint256', support: 'uint8', voter: 'address', nonce: 'uint256' },
ExtendedBallot: {
proposalId: 'uint256',
Expand Down
Loading
Loading