--- 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).