forked from ethereum/EIPs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update EIP-5827: add implementation (ethereum#6474)
* 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
Showing
3 changed files
with
249 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |