150 lines
4.6 KiB
Solidity
150 lines
4.6 KiB
Solidity
// 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;
|
|
}
|
|
}
|