Skip to content

Commit

Permalink
Add EIP-5827: Auto-renewable allowance extension (#5827)
Browse files Browse the repository at this point in the history
* feat: add EIP-5824

* chore: update title

* chore: update links

* fix: EIP number, relative links

* fix: added renamed file

* fix: add EIP-20 reference

* Apply suggestions from code review

Co-authored-by: Pandapip1 <[email protected]>

* feat: keep interface optionally separated from EIP-20

* Apply suggestions from code review

Co-authored-by: Neuti Yoo <[email protected]>

* feat: add optional interfaces

* refactor: renamed event and approve functions

* refactor: added more functions to ERC165 hash

* refactor: comment styles

* fix: added expiration to query function

* feat: change recoveryRate to uint256

* feat: added ERC165 hashes

* fix: trailing space, added  requirement

* feat: added insufficient allowance  error, clarified transfer event

* fix: linter

* fix: replace fancy double-quotes with u+22

* fix: prose style

- avoid acronyms that may be unfamiliar to audience
- reword sentences to flow better

* fix: fix natspec docs in interfaces

- make them actually work
- split lines where required so max width is 100 chars
- prose fixes
- indentation

* feat: add new security consideration about non-EIP-5827-aware apps

e.g. token allowance revocation tools

* fix: specify that the extensions should extend IERC5827 too

* fix: clarify possibly awkward expression

and fix typo in param name

* fix: add Markdown link to first mention of EIP-5827

(kind of weird, though...)

* fix: "MUST not" -> "MUST NOT"

Co-authored-by: Pandapip1 <[email protected]>
Co-authored-by: Neuti Yoo <[email protected]>
Co-authored-by: zhongfu <[email protected]>
  • Loading branch information
4 people authored Nov 16, 2022
1 parent aed2995 commit 3ef3772
Showing 1 changed file with 236 additions and 0 deletions.
236 changes: 236 additions & 0 deletions EIPS/eip-5827.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
---
eip: 5827
title: Auto-renewable allowance extension
description: Extension to enable automatic renewals on allowance approvals
author: zlace (@zlace0x), zhongfu (@zhongfu), edison0xyz (@edison0xyz)
discussions-to: https://ethereum-magicians.org/t/eip-5827-auto-renewable-allowance-extension/10392
status: Draft
type: Standards Track
category: ERC
created: 2022-10-22
requires: 20, 165
---

## Abstract

This extension adds a renewable allowance mechanism to [EIP-20](./eip-20.md) allowances, in which a `recoveryRate` defines the amount of token per second that the allowance regains towards the initial maximum approval `amount`.

## Motivation

Currently, EIP-20 tokens support allowances, with which token owners can allow a spender to spend a certain amount of tokens on their behalf. However, this is not ideal in circumstances involving recurring payments (e.g. subscriptions, salaries, recurring direct-cost-averaging purchases).

Many existing DApps circumvent this limitation by requesting that users grant a large or unlimited allowance. This presents a security risk as malicious DApps can drain users' accounts up to the allowance granted, and users may not be aware of the implications of granting allowances.

An auto-renewable allowance enables many traditional financial concepts like credit and debit limits. An account owner can specify a spending limit, and limit the amount charged to the account based on an allowance that recovers over time.


## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

```solidity
pragma solidity ^0.8.0;
interface IERC5827 /* is ERC20, ERC165 */ {
/*
* Note: the ERC-165 identifier for this interface is 0x93cd7af6.
* 0x93cd7af6 ===
* bytes4(keccak256('approveRenewable(address,uint256,uint256)')) ^
* bytes4(keccak256('renewableAllowance(address,address)')) ^
* bytes4(keccak256('approve(address,uint256)') ^
* bytes4(keccak256('transferFrom(address,address,uint256)') ^
* bytes4(keccak256('allowance(address,address)') ^
*/
/**
* @notice Thrown when the available allowance is less than the transfer amount.
* @param available allowance available; 0 if unset
*/
error InsufficientRenewableAllowance(uint256 available);
/**
* @notice Emitted when any allowance is set.
* @dev MUST be emitted even if a non-renewable allowance is set; if so, the
* @dev `_recoveryRate` MUST be 0.
* @param _owner owner of token
* @param _spender allowed spender of token
* @param _value initial and maximum allowance granted to spender
* @param _recoveryRate recovery amount per second
*/
event RenewableApproval(
address indexed _owner,
address indexed _spender,
uint256 _value,
uint256 _recoveryRate
);
/**
* @notice Grants an allowance of `_value` to `_spender` initially, which recovers over time
* @notice at a rate of `_recoveryRate` up to a limit of `_value`.
* @dev SHOULD cause `allowance(address _owner, address _spender)` to return `_value`,
* @dev SHOULD throw when `_recoveryRate` is larger than `_value`, and MUST emit a
* @dev `RenewableApproval` event.
* @param _spender allowed spender of token
* @param _value initial and maximum allowance granted to spender
* @param _recoveryRate recovery amount per second
*/
function approveRenewable(
address _spender,
uint256 _value,
uint256 _recoveryRate
) external returns (bool success);
/**
* @notice Returns approved max amount and recovery rate of allowance granted to `_spender`
* @notice by `_owner`.
* @dev `amount` MUST also be the initial approval amount when a non-renewable allowance
* @dev has been granted, e.g. with `approve(address _spender, uint256 _value)`.
* @param _owner owner of token
* @param _spender allowed spender of token
* @return amount initial and maximum allowance granted to spender
* @return recoveryRate recovery amount per second
*/
function renewableAllowance(address _owner, address _spender)
external
view
returns (uint256 amount, uint256 recoveryRate);
/// Overridden ERC-20 functions
/**
* @notice Grants a (non-increasing) allowance of _value to _spender and clears any existing
* @notice renewable allowance.
* @dev MUST clear set `_recoveryRate` to 0 on the corresponding renewable allowance, if
* @dev any.
* @param _spender allowed spender of token
* @param _value allowance granted to spender
*/
function approve(address _spender, uint256 _value)
external
returns (bool success);
/**
* @notice Moves `amount` tokens from `from` to `to` using the caller's allowance.
* @dev When deducting `amount` from the caller's allowance, the allowance amount used
* @dev SHOULD include the amount recovered since the last transfer, but MUST NOT exceed
* @dev the maximum allowed amount returned by `renewableAllowance(address _owner, address
* @dev _spender)`.
* @dev SHOULD also throw `InsufficientRenewableAllowance` when the allowance is
* @dev insufficient.
* @param from token owner address
* @param to token recipient
* @param amount amount of token to transfer
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
/**
* @notice Returns amount currently spendable by `_spender`.
* @dev The amount returned MUST be as of `block.timestamp`, if a renewable allowance
* @dev for the `_owner` and `_spender` is present.
* @param _owner owner of token
* @param _spender allowed spender of token
* @return remaining allowance at the current point in time
*/
function allowance(address _owner, address _spender)
external
view
returns (uint256 remaining);
}
```

Base method `approve(address _spender, uint256 _value)` MUST set `recoveryRate` to 0.

Both `allowance()` and `transferFrom()` MUST be updated to include allowance recovery logic.

`approveRenewable(address _spender, uint256 _value, uint256 _recoveryRate)` MUST set both the initial allowance amount and the maximum allowance limit (to which the allowance can recover) to `_value`.

`supportsInterface(0x93cd7af6)` MUST return `true`.

### Additional interfaces

**Token Proxy**

Existing EIP-20 tokens can delegate allowance enforcement to a proxy contract that implements this specification. An additional query function exists to get the underlying EIP-20 token.

```solidity
interface IERC5827Proxy /* is IERC5827 */ {
/*
* Note: the ERC-165 identifier for this interface is 0xc55dae63.
* 0xc55dae63 ===
* bytes4(keccak256('baseToken()')
*/
/**
* @notice Get the underlying base token being proxied.
* @return baseToken address of the base token
*/
function baseToken() external view returns (address);
}
```

The `transfer()` function on the proxy MUST NOT emit the `Transfer` event (as the underlying token already does so).

**Automatic Expiration**

```solidity
interface IERC5827Expirable /* is IERC5827 */ {
/*
* Note: the ERC-165 identifier for this interface is 0x46c5b619.
* 0x46c5b619 ===
* bytes4(keccak256('approveRenewable(address,uint256,uint256,uint64)')) ^
* bytes4(keccak256('renewableAllowance(address,address)')) ^
*/
/**
* @notice Grants an allowance of `_value` to `_spender` initially, which recovers over time
* @notice at a rate of `_recoveryRate` up to a limit of `_value` and expires at
* @notice `_expiration`.
* @dev SHOULD throw when `_recoveryRate` is larger than `_value`, and MUST emit
* @dev `RenewableApproval` event.
* @param _spender allowed spender of token
* @param _value initial allowance granted to spender
* @param _recoveryRate recovery amount per second
* @param _expiration Unix time (in seconds) at which the allowance expires
*/
function approveRenewable(
address _spender,
uint256 _value,
uint256 _recoveryRate,
uint64 _expiration
) external returns (bool success);
/**
* @notice Returns approved max amount, recovery rate, and expiration timestamp.
* @return amount initial and maximum allowance granted to spender
* @return recoveryRate recovery amount per second
* @return expiration Unix time (in seconds) at which the allowance expires
*/
function renewableAllowance(address _owner, address _spender)
external
view
returns (uint256 amount, uint256 recoveryRate, uint64 expiration);
}
```

## Rationale

Renewable allowances can be implemented with discrete resets per time cycle. However, a continuous `recoveryRate` allows for more flexible use cases not bound by reset cycles and can be implemented with simpler logic.

## Backwards Compatibility

Existing EIP-20 token contracts can delegate allowance enforcement to a proxy contract that implements this specification.

## Security Considerations

This EIP introduces a stricter set of constraints compared to EIP-20 with unlimited allowances. However, when `_recoveryRate` is set to a large value, large amounts can still be transferred over multiple transactions.

Applications that are not [EIP-5827](./eip-5827.md)-aware may erroneously infer that the value returned by `allowance(address _owner, address _spender)` or included in `Approval` events is the maximum amount of tokens that `_spender` can spend from `_owner`. This may not be the case, such as when a renewable allowance is granted to `_spender` by `_owner`.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).

0 comments on commit 3ef3772

Please sign in to comment.