Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Add allowance feature to EIP-5725 #6

Merged
merged 3 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 107 additions & 62 deletions EIPS/eip-5725.md

Large diffs are not rendered by default.

163 changes: 117 additions & 46 deletions assets/eip-5725/contracts/ERC5725.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ abstract contract ERC5725 is IERC5725, ERC721 {
using SafeERC20 for IERC20;

/// @dev mapping for claimed payouts
mapping(uint256 => uint256) /*tokenId*/ /*claimed*/
internal _payoutClaimed;
mapping(uint256 => uint256) /*tokenId*/ /*claimed*/ internal _payoutClaimed;

/// @dev Mapping from token ID to approved tokenId operator
mapping(uint256 => address) private _tokenIdApprovals;

/// @dev Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) /* owner */ /*(operator, isApproved)*/ internal _operatorApprovals;

/**
* @notice Checks if the tokenId exists and its valid
Expand All @@ -28,7 +33,8 @@ abstract contract ERC5725 is IERC5725, ERC721 {
* @dev See {IERC5725}.
*/
function claim(uint256 tokenId) external override(IERC5725) validToken(tokenId) {
require(ownerOf(tokenId) == msg.sender, "Not owner of NFT");
require(isApprovedClaimOrOwner(msg.sender, tokenId), "ERC5725: not owner or operator");

uint256 amountClaimed = claimablePayout(tokenId);
require(amountClaimed > 0, "ERC5725: No pending payout");

Expand All @@ -38,6 +44,26 @@ abstract contract ERC5725 is IERC5725, ERC721 {
IERC20(payoutToken(tokenId)).safeTransfer(msg.sender, amountClaimed);
}

/**
* @dev See {IERC5725}.
*/
function setClaimApprovalForAll(address operator, bool approved) external override(IERC5725) {
_setClaimApprovalForAll(operator, approved);
emit ClaimApprovalForAll(msg.sender, operator, approved);
}

/**
* @dev See {IERC5725}.
*/
function setClaimApproval(
address operator,
bool approved,
uint256 tokenId
) external override(IERC5725) validToken(tokenId) {
_setClaimApproval(operator, tokenId);
emit ClaimApproval(msg.sender, operator, tokenId, approved);
}

/**
* @dev See {IERC5725}.
*/
Expand All @@ -48,62 +74,44 @@ abstract contract ERC5725 is IERC5725, ERC721 {
/**
* @dev See {IERC5725}.
*/
function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)
public
view
virtual
override(IERC5725)
returns (uint256 payout);
function vestedPayoutAtTime(
uint256 tokenId,
uint256 timestamp
) public view virtual override(IERC5725) returns (uint256 payout);

/**
* @dev See {IERC5725}.
*/
function vestingPayout(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
returns (uint256 payout)
{
function vestingPayout(
uint256 tokenId
) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
return _payout(tokenId) - vestedPayout(tokenId);
}

/**
* @dev See {IERC5725}.
*/
function claimablePayout(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
returns (uint256 payout)
{
function claimablePayout(
uint256 tokenId
) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
return vestedPayout(tokenId) - _payoutClaimed[tokenId];
}

/**
* @dev See {IERC5725}.
*/
function claimedPayout(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
returns (uint256 payout)
{
function claimedPayout(
uint256 tokenId
) public view override(IERC5725) validToken(tokenId) returns (uint256 payout) {
return _payoutClaimed[tokenId];
}

/**
* @dev See {IERC5725}.
*/
function vestingPeriod(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
returns (uint256 vestingStart, uint256 vestingEnd)
{
function vestingPeriod(
uint256 tokenId
) public view override(IERC5725) validToken(tokenId) returns (uint256 vestingStart, uint256 vestingEnd) {
return (_startTime(tokenId), _endTime(tokenId));
}

Expand All @@ -116,18 +124,81 @@ abstract contract ERC5725 is IERC5725, ERC721 {

/**
* @dev See {IERC165-supportsInterface}.
* IERC5725 interfaceId = 0x7c89676d
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC721, IERC165)
returns (bool supported)
{
* IERC5725 interfaceId = 0xbd3a202b
*/
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721, IERC165) returns (bool supported) {
return interfaceId == type(IERC5725).interfaceId || super.supportsInterface(interfaceId);
}

/**
* @dev See {IERC5725}.
*/
function getClaimApproved(uint256 tokenId) public view returns (address operator) {
return _tokenIdApprovals[tokenId];
}

/**
* @dev Returns true if `owner` has set `operator` to manage all `tokenId`s.
* @param owner The owner allowing `operator` to manage all `tokenId`s.
* @param operator The address who is given permission to spend tokens on behalf of the `owner`.
*/
function isClaimApprovedForAll(address owner, address operator) public view returns (bool isClaimApproved) {
return _operatorApprovals[owner][operator];
}

/**
* @dev Public view which returns true if the operator has permission to claim for `tokenId`
* @notice To remove permissions, set operator to zero address.
*
* @param operator The address that has permission for a `tokenId`.
* @param tokenId The NFT `tokenId`.
*/
function isApprovedClaimOrOwner(address operator, uint256 tokenId) public view virtual returns (bool) {
address owner = ownerOf(tokenId);
return (operator == owner || isClaimApprovedForAll(owner, operator) || getClaimApproved(tokenId) == operator);
}

/**
* @dev Internal function to set the operator status for a given owner to manage all `tokenId`s.
* @notice To remove permissions, set approved to false.
*
* @param operator The address who is given permission to spend vested tokens.
* @param approved The approved status.
*/
function _setClaimApprovalForAll(address operator, bool approved) internal virtual {
_operatorApprovals[msg.sender][operator] = approved;
}

/**
* @dev Internal function to set the operator status for a given tokenId.
* @notice To remove permissions, set operator to zero address.
*
* @param operator The address who is given permission to spend vested tokens.
* @param tokenId The NFT `tokenId`.
*/
function _setClaimApproval(address operator, uint256 tokenId) internal virtual {
require(ownerOf(tokenId) == msg.sender, "ERC5725: not owner of tokenId");
_tokenIdApprovals[tokenId] = operator;
}

/**
* @dev Internal function to hook into {IERC721-_afterTokenTransfer}, when a token is being transferred.
* Removes permissions to _tokenIdApprovals[tokenId] when the tokenId is transferred, burnt, but not on mint.
*
* @param from The address from which the tokens are being transferred.
* @param to The address to which the tokens are being transferred.
* @param firstTokenId The first tokenId in the batch that is being transferred.
* @param batchSize The number of tokens being transferred in this batch.
*/
function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override {
super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
if (from != address(0)) {
delete _tokenIdApprovals[firstTokenId];
}
}

/**
* @dev Internal function to get the payout token of a given vesting NFT
*
Expand Down
113 changes: 80 additions & 33 deletions assets/eip-5725/contracts/IERC5725.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,88 +3,135 @@ pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

/**
* @title Non-Fungible Vesting Token Standard
* @title Non-Fungible Vesting Token Standard.
* @notice A non-fungible token standard used to vest ERC-20 tokens over a vesting release curve
* scheduled using timestamps.
* @dev Because this standard relies on timestamps for the vesting schedule, it's important to keep track of the
* tokens claimed per Vesting NFT so that a user cannot withdraw more tokens than alloted for a specific Vesting NFT.
* tokens claimed per Vesting NFT so that a user cannot withdraw more tokens than allotted for a specific Vesting NFT.
* @custom:interface-id 0xbd3a202b
*/
interface IERC5725 is IERC721 {
/**
* This event is emitted when the payout is claimed through the claim function
* This event is emitted when the payout is claimed through the claim function.
* @param tokenId the NFT tokenId of the assets being claimed.
* @param recipient The address which is receiving the payout.
* @param claimAmount The amount of tokens being claimed.
*/
event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 claimAmount);

/**
* @notice Claim the pending payout for the NFT
* @dev MUST grant the claimablePayout value at the time of claim being called
* MUST revert if not called by the token owner or approved users
* MUST emit PayoutClaimed
* SHOULD revert if there is nothing to claim
* @param tokenId The NFT token id
* This event is emitted when an `owner` sets an address to manage token claims for all tokens.
* @param owner The address setting a manager to manage all tokens.
* @param spender The address being permitted to manage all tokens.
* @param approved A boolean indicating whether the spender is approved to claim for all tokens.
*/
event ClaimApprovalForAll(address indexed owner, address indexed spender, bool approved);

/**
* This event is emitted when an `owner` sets an address to manage token claims for a `tokenId`.
* @param owner The `owner` of `tokenId`.
* @param spender The address being permitted to manage a tokenId.
* @param tokenId The unique identifier of the token being managed.
* @param approved A boolean indicating whether the spender is approved to claim for `tokenId`.
*/
event ClaimApproval(address indexed owner, address indexed spender, uint256 indexed tokenId, bool approved);

/**
* @notice Claim the pending payout for the NFT.
* @dev MUST grant the claimablePayout value at the time of claim being called to `msg.sender`.
* MUST revert if not called by the token owner or approved users.
* MUST emit PayoutClaimed.
* SHOULD revert if there is nothing to claim.
* @param tokenId The NFT token id.
*/
function claim(uint256 tokenId) external;

/**
* @notice Number of tokens for the NFT which have been claimed at the current timestamp
* @param tokenId The NFT token id
* @return payout The total amount of payout tokens claimed for this NFT
* @notice Number of tokens for the NFT which have been claimed at the current timestamp.
* @param tokenId The NFT token id.
* @return payout The total amount of payout tokens claimed for this NFT.
*/
function claimedPayout(uint256 tokenId) external view returns (uint256 payout);

/**
* @notice Number of tokens for the NFT which can be claimed at the current timestamp
* @notice Number of tokens for the NFT which can be claimed at the current timestamp.
* @dev It is RECOMMENDED that this is calculated as the `vestedPayout()` subtracted from `payoutClaimed()`.
* @param tokenId The NFT token id
* @return payout The amount of unlocked payout tokens for the NFT which have not yet been claimed
* @param tokenId The NFT token id.
* @return payout The amount of unlocked payout tokens for the NFT which have not yet been claimed.
*/
function claimablePayout(uint256 tokenId) external view returns (uint256 payout);

/**
* @notice Total amount of tokens which have been vested at the current timestamp.
* This number also includes vested tokens which have been claimed.
* @dev It is RECOMMENDED that this function calls `vestedPayoutAtTime` with
* `block.timestamp` as the `timestamp` parameter.
* @param tokenId The NFT token id
* This number also includes vested tokens which have been claimed.
* @dev It is RECOMMENDED that this function calls `vestedPayoutAtTime`
* with `block.timestamp` as the `timestamp` parameter.
* @param tokenId The NFT token id.
* @return payout Total amount of tokens which have been vested at the current timestamp.
*/
function vestedPayout(uint256 tokenId) external view returns (uint256 payout);

/**
* @notice Total amount of vested tokens at the provided timestamp.
* This number also includes vested tokens which have been claimed.
* This number also includes vested tokens which have been claimed.
* @dev `timestamp` MAY be both in the future and in the past.
* Zero MUST be returned if the timestamp is before the token was minted.
* @param tokenId The NFT token id
* @param timestamp The timestamp to check on, can be both in the past and the future
* @return payout Total amount of tokens which have been vested at the provided timestamp
* Zero MUST be returned if the timestamp is before the token was minted.
* @param tokenId The NFT token id.
* @param timestamp The timestamp to check on, can be both in the past and the future.
* @return payout Total amount of tokens which have been vested at the provided timestamp.
*/
function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) external view returns (uint256 payout);

/**
* @notice Number of tokens for an NFT which are currently vesting.
* @dev The sum of vestedPayout and vestingPayout SHOULD always be the total payout.
* @param tokenId The NFT token id
* @param tokenId The NFT token id.
* @return payout The number of tokens for the NFT which are vesting until a future date.
*/
function vestingPayout(uint256 tokenId) external view returns (uint256 payout);

/**
* @notice The start and end timestamps for the vesting of the provided NFT
* MUST return the timestamp where no further increase in vestedPayout occurs for `vestingEnd`.
* @param tokenId The NFT token id
* @return vestingStart The beginning of the vesting as a unix timestamp
* @return vestingEnd The ending of the vesting as a unix timestamp
* @notice The start and end timestamps for the vesting of the provided NFT.
* MUST return the timestamp where no further increase in vestedPayout occurs for `vestingEnd`.
* @param tokenId The NFT token id.
* @return vestingStart The beginning of the vesting as a unix timestamp.
* @return vestingEnd The ending of the vesting as a unix timestamp.
*/
function vestingPeriod(uint256 tokenId) external view returns (uint256 vestingStart, uint256 vestingEnd);

/**
* @notice Token which is used to pay out the vesting claims
* @param tokenId The NFT token id
* @return token The token which is used to pay out the vesting claims
* @notice Token which is used to pay out the vesting claims.
* @param tokenId The NFT token id.
* @return token The token which is used to pay out the vesting claims.
*/
function payoutToken(uint256 tokenId) external view returns (address token);

/**
* @notice Sets a global `operator` with permission to manage all tokens owned by the current `msg.sender`.
* @param operator The address to let manage all tokens.
* @param approved A boolean indicating whether the spender is approved to claim for all tokens.
*/
function setClaimApprovalForAll(address operator, bool approved) external;

/**
* @notice Sets a tokenId `operator` with permission to manage a single `tokenId` owned by the `msg.sender`.
* @param operator The address to let manage a single `tokenId`.
* @param tokenId the `tokenId` to be managed.
* @param approved A boolean indicating whether the spender is approved to claim for all tokens.
*/
function setClaimApproval(address operator, bool approved, uint256 tokenId) external;

/**
* @notice Returns true if `owner` has set `operator` to manage all `tokenId`s.
* @param owner The owner allowing `operator` to manage all `tokenId`s.
* @param operator The address who is given permission to spend tokens on behalf of the `owner`.
*/
function isClaimApprovedForAll(address owner, address operator) external view returns (bool isClaimApproved);

/**
* @notice Returns the operating address for a `tokenId`.
* If `tokenId` is not managed, then returns the zero address.
* @param tokenId The NFT `tokenId` to query for a `tokenId` manager.
*/
function getClaimApproved(uint256 tokenId) external view returns (address operator);
}
Loading