Skip to content

Commit

Permalink
Update EIP-5827: add implementation (ethereum#6474)
Browse files Browse the repository at this point in the history
* feat: add EIP-5827 implementation

* fix: title and remove link

* fix: added ref implementation as assets, rename EIP prefix to ERC

* fix: md lint
  • Loading branch information
zlace0x committed Mar 9, 2023
1 parent 9ce1afe commit ea8c900
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 6 deletions.
18 changes: 12 additions & 6 deletions EIPS/eip-5827.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ 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`.
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, 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).
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.

Expand Down Expand Up @@ -154,7 +154,7 @@ Both `allowance()` and `transferFrom()` MUST be updated to include allowance rec

**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.
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 */ {
Expand Down Expand Up @@ -223,13 +223,19 @@ Renewable allowances can be implemented with discrete resets per time cycle. How

## Backwards Compatibility

Existing EIP-20 token contracts can delegate allowance enforcement to a proxy contract that implements this specification.
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 EIP-20 with unlimited allowances. However, when `_recoveryRate` is set to a large value, large amounts can still be transferred over multiple transactions.
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 [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`.
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

Expand Down
149 changes: 149 additions & 0 deletions assets/eip-5827/ERC5827.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.17;

import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "./IERC5827.sol";

contract ERC5827 is ERC20, IERC5827 {
struct RenewableAllowance {
uint256 amount;
uint192 recoveryRate;
uint64 lastUpdated;
}

// owner => spender => renewableAllowance
mapping(address => mapping(address => RenewableAllowance))
private rAllowance;

constructor(
string memory name_,
string memory symbol_
) ERC20(name_, symbol_) {}

function approve(
address _spender,
uint256 _value
) public override(ERC20, IERC5827) returns (bool success) {
address owner = _msgSender();
_approve(owner, _spender, _value, 0);
return true;
}

function approveRenewable(
address _spender,
uint256 _value,
uint256 _recoveryRate
) public override returns (bool success) {
address owner = _msgSender();
_approve(owner, _spender, _value, _recoveryRate);
return true;
}

function _approve(
address _owner,
address _spender,
uint256 _value,
uint256 _recoveryRate
) internal virtual {
require(
_recoveryRate <= _value,
"recoveryRate must be less than or equal to value"
);

rAllowance[_owner][_spender] = RenewableAllowance({
amount: _value,
recoveryRate: uint192(_recoveryRate),
lastUpdated: uint64(block.timestamp)
});

_approve(_owner, _spender, _value);
emit RenewableApproval(_owner, _spender, _value, _recoveryRate);
}

/// @notice fetch amounts spendable by _spender
/// @return remaining allowance at the current point in time
function allowance(
address _owner,
address _spender
) public view override(ERC20, IERC5827) returns (uint256 remaining) {
return _remainingAllowance(_owner, _spender);
}

/// @dev returns the sum of two uint256 values, saturating at 2**256 - 1
function saturatingAdd(
uint256 a,
uint256 b
) internal pure returns (uint256) {
unchecked {
uint256 c = a + b;
if (c < a) return type(uint256).max;
return c;
}
}

function _remainingAllowance(
address _owner,
address _spender
) private view returns (uint256) {
RenewableAllowance memory a = rAllowance[_owner][_spender];
uint256 remaining = super.allowance(_owner, _spender);

uint256 recovered = uint256(a.recoveryRate) *
uint64(block.timestamp - a.lastUpdated);
uint256 remainingAllowance = saturatingAdd(remaining, recovered);
return remainingAllowance > a.amount ? a.amount : remainingAllowance;
}

/// @notice fetch approved max amount and recovery rate
/// @return amount initial and maximum allowance given to spender
/// @return recoveryRate recovery amount per second
function renewableAllowance(
address _owner,
address _spender
) public view returns (uint256 amount, uint256 recoveryRate) {
RenewableAllowance memory a = rAllowance[_owner][_spender];
return (a.amount, uint256(a.recoveryRate));
}

/// @notice transfers base token with renewable allowance logic applied
/// @param from owner of base token
/// @param to recipient of base token
/// @param amount amount to transfer
function transferFrom(
address from,
address to,
uint256 amount
) public override(ERC20, IERC5827) returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}

function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual override {
(uint256 maxAllowance, ) = renewableAllowance(owner, spender);
if (maxAllowance != type(uint256).max) {
uint256 currentAllowance = _remainingAllowance(owner, spender);
if (currentAllowance < amount) {
revert InsufficientRenewableAllowance({
available: currentAllowance
});
}

unchecked {
_approve(owner, spender, currentAllowance - amount);
}
rAllowance[owner][spender].lastUpdated = uint64(block.timestamp);
}
}

function supportsInterface(
bytes4 interfaceId
) public view virtual returns (bool) {
return interfaceId == type(IERC5827).interfaceId;
}
}
88 changes: 88 additions & 0 deletions assets/eip-5827/IERC5827.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "openzeppelin-contracts/interfaces/IERC20.sol";
import "openzeppelin-contracts/interfaces/IERC165.sol";

/// @title Interface for IERC5827 contracts
/// @notice Please see https://eips.ethereum.org/EIPS/eip-5827 for more details on the goals of this interface
/// @author Zac (zlace0x), zhongfu (zhongfu), Edison (edison0xyz)
interface IERC5827 is IERC20, IERC165 {
/// 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)') ^

/// @dev Thrown when there available allowance is lesser than transfer amount
/// @param available Allowance available, 0 if unset
error InsufficientRenewableAllowance(uint256 available);

/// @notice Emitted when a new renewable allowance is set.
/// @param _owner owner of token
/// @param _spender allowed spender of token
/// @param _value initial and maximum allowance given 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 based on `_recoveryRate` up to a limit of `_value`.
/// SHOULD throw when `_recoveryRate` is larger than `_value`.
/// MUST emit `RenewableApproval` event.
/// @param _spender allowed spender of token
/// @param _value initial and maximum allowance given 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.
/// @return amount initial and maximum allowance given to spender
/// @return recoveryRate recovery amount per second
function renewableAllowance(
address _owner,
address _spender
) external view returns (uint256 amount, uint256 recoveryRate);

/// Overridden EIP-20 functions

/// @notice Grants a (non-increasing) allowance of _value to _spender.
/// MUST clear set _recoveryRate to 0 on the corresponding renewable allowance, if any.
/// @param _spender allowed spender of token
/// @param _value allowance given to spender
function approve(
address _spender,
uint256 _value
) external returns (bool success);

/// @notice Moves `amount` tokens from `from` to `to` using the
/// allowance mechanism. `amount` is then deducted from the caller's
/// allowance factoring in recovery rate logic.
/// SHOULD throw when there is insufficient allowance
/// @param from token owner address
/// @param to token recipient
/// @param amount amount of token to transfer
/// @return success True if the function is successful, false if otherwise
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool success);

/// @notice Returns amounts spendable by `_spender`.
/// @param _owner Address of the owner
/// @param _spender spender of token
/// @return remaining allowance at the current point in time
function allowance(
address _owner,
address _spender
) external view returns (uint256 remaining);
}

0 comments on commit ea8c900

Please sign in to comment.