DCIPs/EIPS/eip-5827.md

243 lines
10 KiB
Markdown
Raw Normal View History

---
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 [ERC-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, ERC-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 ERC-20 tokens can delegate allowance enforcement to a proxy contract that implements this specification. An additional query function exists to get the underlying ERC-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 ERC-20 token contracts can delegate allowance enforcement to a proxy contract that implements this specification.
## Reference Implementation
An minimal implementation is included [here](../assets/eip-5827/ERC5827.sol)
An audited, open source implemention of this standard as a `IERC5827Proxy` can be found at `https://github.com/suberra/funnel-contracts`
## Security Considerations
This EIP introduces a stricter set of constraints compared to ERC-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 [ERC-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).