ERC-3009: Transfer with Authorization

Old discussion: EIP-3009: "Transfer With Authorization" - ERC20 meta-transactions · Issue #3010 · ethereum/EIPs · GitHub

Unstagnent pull request: Update ERC-3009: Move to Final by petejkim · Pull Request #1241 · ethereum/ERCs · GitHub

Hey everyone! I’d like to raise a related point about the validity range parameters.

The current spec describes validAfter and validBefore as:

”The time after which this is valid (unix time)”

This is stated in the @param NatSpec (not as a normative requirement), but it effectively locks implementations into timestamp-based comparison. The reference implementation reinforces this by comparing against block.timestamp.

I understand that making it consistently block.timestamp makes it clear for the users, otherwise there’s no way to know whether they’re specifying validity for a block.number. However, there are a couple of cases where using block numbers might be relevant. For example, if an ERC20 token follows ERC-6372’s clock, I believe the authorization’s validity should be aligned with the token.

Another example is the ERC-4337 entrypoint, that allows setting the leftmost bit to 1 to specify that the validity range is for block numbers instead of timestamps.

Would you consider removing “unix time” from the @param descriptions and instead leave the time unit to the implementation?

Hey! Following up on my earlier comment about the validAfter/validBefore time domain, I’d like to raise another related point the nonce scheme.

Currently the ERC motivates the use of random `bytes32` nonces to solve the sequentiality issue in ERC-2612, allowing users to create a many concurrent authorizations as they need.

Following the ERC’s motivation, the underlying goal of nonce parallelism can also be achieve with the keyed sequential nonce scheme introduced by ERC-4337 (and supported by NoncesKeyed). Even in that case randomized `bytes32` would be supported by choosing a different nonce key.

One example where users might prefer using sequential nonces vs randomized ones (aside from enforcing order) would be to avoid paying for a fresh cold storage slot (~20k gas) rather than incrementing an already used one.

Would you be open to either:

  • Relaxing the language to acknowledge that any scheme guaranteeing non-replayability and supporting concurrent authorizations is compliant, including keyed sequential nonces; or
  • Explicitly calling out the ERC-4337 keyed nonce model as a valid alternative to fully random nonces.

One tradeoff worth being transparent about: with keyed sequential nonces, cancelAuthorization can only cancel the next pending nonce in a key’s sequence. You can’t skip ahead and cancel a future nonce without first consuming or cancelling its predecessors. Users wanting to cancel a single future authorization in isolation should use a dedicated single-use key for it, which preserves the same semantics as random nonces.

Hey @ernestognw, thanks for raising both points!


ERC4337-Like Parallel Sequential Nonces

Main thoughts:

  1. Sequential nonces are cheaper than random nonces, since they don’t need to initialize empty storage slots on every operation.
  2. Sequential nonces allow conditional execution, since nonce N+1 cannot be executed until nonce N is.
  3. Sequential nonces can allow bulk invalidation with a single signature, since increasing the last consumed nonce from N to N+3 invalidates N+1, N+2, and N+3 in a single operation.
  4. Conditional execution has more limited use-cases in ERC20 token transfers than in ERC4337 operations.
    1. An ERC4337 workflow can be:
      1. Execute governance action.
      2. Grant self-owned role to a different account.
      3. Renounce role from own account.
    2. ERC20 transfer workflows usually take the form of periodic or scheduled payments. Bulk invalidations are most desirable here, although the use-case seems more niche.
  5. ERC4337-style parallel sequential nonces are equivalent to ERC3009, since 24-byte salts can be considered equivalent to 32-byte salts.
  6. If using ERC4337-style parallel sequential nonces are used, existing ERC3009 integrations would need to use zeroes in the last 8 bytes of their nonces. Otherwise, the operations would fail.

Therefore:

  • A separate function authorizationNonce(address authorizer, uint192 sequence) external view returns (uint256 nonce) / nonces / getNonce is required to get the nonce for a given sequence. authorizationState would be redundant and could be omitted.
  • Updating cancelAuthorization to only allow next-nonce in sequence cancellation would be a breaking change. For cancelAuthorization to remain equivalent, i.e., allowing the cancellation of any nonce in a sequence without invalidating the previous ones; requires more complex storage and nonce consumption mechanisms. A different increaseAuthorizationNonce / increaseNonce / cancelAuthorization function that invalidates previous nonces for a sequence with a single signature would be most desirable, but it would break the implicit assumption that the operation only invalidates a discrete nonce.

Given the spec stance against sequential nonces for their impractical off-chain coordination implications, it seems counter-intuitive to pursue them within the spec as a non-breaking change, since they have side-effects that would compromise existing integrations:

  • The last 8 bytes of the generated nonces would need to be zero.
  • cancelAuthorization behaviour would differ or a more complex nonce consumption mechanism would need to be implemented.

As the spec is still not finalized, I would consider implementing parallel sequential nonces as a breaking change within ERC3009, similar to ERC1271 evolution.

Otherwise, I would consider proposing this as a new ERC3009 extension spec. Similar to ERC7598, although with only partial backward compatibility.


Validity Clock

I don’t have a strong opinion here, but if we shift the decision from the spec to the implementation, integrators now need to wonder in what units to express the deadlines.

I would consider specifying it in the standard: adopting ERC6372 to use block.number with a fallback to block.timestamp if ERC6372 is not present, or enforcing ERC4337-like behaviour, for example.