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.
ERC4353: interface for staked NFTs (ethereum#4353) (ethereum#5103)
Apply suggestions from code review Co-authored-by: Micah Zoltu <micah@zoltu.net> Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Co-authored-by: Micah Zoltu <micah@zoltu.net> Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com>
- Loading branch information
1 parent
a94cd0a
commit 361de89
Showing
1 changed file
with
230 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
--- | ||
eip: 4353 | ||
title: Interface for Staked Tokens in NFTs | ||
description: This interface enables access to publicly viewable staking data of an NFT. | ||
author: Rex Creed (@aug2uag), Dane Scarborough <dane@nftapps.us> | ||
discussions-to: https://ethereum-magicians.org/t/eip-4353-viewing-staked-tokens-in-nft/7234 | ||
status: Draft | ||
type: Standards Track | ||
category: ERC | ||
created: 2021-10-08 | ||
requires: 165 | ||
--- | ||
|
||
## Abstract | ||
[EIP-721](./eip-721.md) tokens can be deposited or staked in NFTs for a variety of reasons including escrow, rewards, benefits, and others. There is currently no means of retrieving the number of tokens staked and/or bound to an NFT. This proposal outlines a standard that may be implemented by all wallets and marketplaces easily to correctly retrieve the staked token amount of an NFT. | ||
|
||
## Motivation | ||
Without staked token data, the actual amount of staked tokens cannot be conveyed from token owners to other users, and cannot be displayed in wallets, marketplaces, or block explorers. The ability to identify and verify an exogenous value derived from the staking process may be critical to the aims of an NFT holder. | ||
|
||
## Specification | ||
```solidity | ||
// SPDX-License-Identifier: CC0-1.0 | ||
pragma solidity ^0.8.0; | ||
/** | ||
* @dev Interface of the ERC4353 standard, as defined in the | ||
* https://eips.ethereum.org/EIPS/eip-4353. | ||
* | ||
* Implementers can declare support of contract interfaces, which can then be | ||
* queried by others. | ||
* | ||
* Note: The ERC-165 identifier for this interface is 0x3a3d855f. | ||
* | ||
*/ | ||
interface IERC721Staked { | ||
/** | ||
* @dev Returns uint256 amount of on-chain tokens staked to the NFT. | ||
* | ||
* @dev Wallets and marketplaces would need to call this for displaying | ||
* the amount of tokens staked and/or bound to the NFT. | ||
*/ | ||
function stakedAmount(uint256 tokenId) external view returns (uint256); | ||
} | ||
``` | ||
|
||
### Suggested flow: | ||
|
||
#### Constructor/deployment | ||
* Creator - the owner of an NFT with its own rules for depositing tokens at and/or after the minting of a token. | ||
* Token Amount - the current amount of on-chain [EIP-20](./eip-20.md) or derived tokens bound to an NFT from one or more deposits. | ||
* Withdraw Mechanism - rules based approach for withdrawing staked tokens and making sure to update the balance of the staked tokens. | ||
|
||
### Staking at mint and locking tokens in NFT | ||
The suggested and intended implementation of this standard is to stake tokens at the time of minting an NFT, and not implementing any outbound transfer of tokens outside of `burn`. Therefore, only to stake at minting and withdraw only at burning. | ||
|
||
#### NFT displayed in wallet or marketplace | ||
A wallet or marketplace checks if an NFT has publicly staked tokens available for display - if so, call `stakedAmount(tokenId)` to get the current amount of tokens staked and/or bound to the NFT. | ||
|
||
The logical code looks something like this and inspired by William Entriken: | ||
|
||
```solidity | ||
// contracts/Token.sol | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
/** | ||
* @title Token | ||
* @dev Very simple ERC721 example with stake interface example. | ||
* Note this implementation enforces recommended procedure: | ||
* 1) stake at mint | ||
* 2) withdraw at burn | ||
*/ | ||
contract ERC721Staked is ERC721URIStorage, Ownable { | ||
/// @dev track original minter of tokenId | ||
mapping (uint256 => address payable) private payees; | ||
/// @dev map tokens to stored staked token value | ||
mapping (uint256 => uint256) private tokenValue; | ||
/// @dev metadata | ||
constructor() ERC721 ( | ||
"Staked NFT", | ||
"SNFT" | ||
){} | ||
/// @dev mints a new NFT | ||
/// @param _to address that will own the minted NFT | ||
/// @param _tokenId id the NFT | ||
/// @param _uri metadata | ||
function mint( | ||
address payable _to, | ||
uint256 _tokenId, | ||
string calldata _uri | ||
) | ||
external | ||
payable | ||
onlyOwner | ||
{ | ||
_mint(_to, _tokenId); | ||
_setTokenURI(_tokenId, _uri); | ||
payees[_tokenId] = _to; | ||
tokenValue[_tokenId] = msg.value; | ||
} | ||
/// @dev staked interface | ||
/// @param _tokenId id of the NFT | ||
/// @return _value staked value | ||
function stakedAmount( | ||
uint256 _tokenId | ||
) external view returns (uint256 _value) { | ||
_value = tokenValue[_tokenId]; | ||
return _value; | ||
} | ||
/// @dev removes NFT & transfers crypto to minter | ||
/// @param _tokenId the NFT we want to remove | ||
function burn( | ||
uint256 _tokenId | ||
) | ||
external | ||
onlyOwner | ||
{ | ||
super._burn(_tokenId); | ||
payees[_tokenId].transfer(tokenValue[_tokenId]); | ||
tokenValue[_tokenId] = 0; | ||
} | ||
} | ||
``` | ||
|
||
## Backward Compatibility | ||
TBD | ||
|
||
## Rationale | ||
This standard is completely agnostic to how tokens are deposited or handled by the NFT. It is, therefore, the choice and responsibility of the author to encode and communicate the encoding of their tokenomics to purchasees of their token and/or to make their contracts viewable by purchasees. | ||
|
||
Although the intention of this standard is for tokens staked at mint and withdrawable only upon burn, the interface may be modified for dynamic withdrawing and depositing of tokens especially under DeFi application settings. In its current form, the contract logic may be the determining factor whether a deviation from the standard exists. | ||
|
||
## Test Cases | ||
```js | ||
const { expect } = require("chai"); | ||
const { ethers, waffle } = require("hardhat"); | ||
const provider = waffle.provider; | ||
|
||
describe("StakedNFT", function () { | ||
let _id = 1234567890; | ||
let value = '1.5'; | ||
let Token; | ||
let Interface; | ||
let owner; | ||
let addr1; | ||
let addr2; | ||
|
||
beforeEach(async function () { | ||
Token = await ethers.getContractFactory("ERC721Staked"); | ||
[owner, addr1, ...addr2] = await ethers.getSigners(); | ||
Interface = await Token.deploy(); | ||
}); | ||
|
||
describe("Staked NFT", function () { | ||
it("Should set the right owner", async function () { | ||
let mint = await Interface.mint( | ||
addr1.address, _id, 'http://foobar') | ||
expect(await Interface.ownerOf(_id)).to.equal(addr1.address); | ||
}); | ||
|
||
it("Should not have staked balance without value", async function () { | ||
let mint = await Interface.mint( | ||
addr1.address, _id, 'http://foobar') | ||
expect(await Interface.stakedAmount(_id)).to.equal( | ||
ethers.utils.parseEther('0')); | ||
}); | ||
|
||
it("Should set and return the staked amount", async function () { | ||
let mint = await Interface.mint( | ||
addr1.address, _id, 'http://foobar', | ||
{value: ethers.utils.parseEther(value)}) | ||
expect(await Interface.stakedAmount(_id)).to.equal( | ||
ethers.utils.parseEther(value)); | ||
}); | ||
|
||
it("Should decrease owner eth balance on mint (deposit)", async function () { | ||
let balance1 = await provider.getBalance(owner.address); | ||
let mint = await Interface.mint( | ||
addr1.address, _id, 'http://foobar', | ||
{value: ethers.utils.parseEther(value)}) | ||
let balance2 = await provider.getBalance(owner.address); | ||
let diff = parseFloat(ethers.utils.formatEther( | ||
balance1.sub(balance2))).toFixed(1); | ||
expect(diff === value); | ||
}); | ||
|
||
it("Should add to payee's eth balance on burn (withdraw)", async function () { | ||
let balance1 = await provider.getBalance(addr1.address); | ||
let mint = await Interface.mint( | ||
addr1.address, _id, 'http://foobar', | ||
{value: ethers.utils.parseEther(value)}) | ||
await Interface.burn(_id); | ||
let balance2 = await provider.getBalance(addr1.address); | ||
let diff = parseFloat(ethers.utils.formatEther( | ||
balance2.sub(balance1))).toFixed(1); | ||
expect(diff === value); | ||
}); | ||
|
||
it("Should update balance after transfer", async function () { | ||
let mint = await Interface.mint( | ||
addr1.address, _id, 'http://foobar', | ||
{value: ethers.utils.parseEther(value)}) | ||
await Interface.burn(_id); | ||
expect(await Interface.stakedAmount(_id)).to.equal( | ||
ethers.utils.parseEther('0')); | ||
}); | ||
}); | ||
}); | ||
``` | ||
|
||
## Security Considerations | ||
The purpose of this standard is to simply and publicly identify whether an NFT claims to have staked tokens. | ||
|
||
Staked claims will be unreliable without a locking mechanism enforced, for example, if staked tokens can only be transferred at burn. Otherwise, tokens may be deposited and/or withdrawn at any time via arbitrary methods. Also, contracts that may allow arbitrary transfers without updating the correct balance will result in potential issues. A strict rules-based approach should be taken with these edge cases in mind. | ||
|
||
A dedicated service may exist to verify the claims of a token by analyzing transactions on the explorer. In this manner, verification may be automated to ensure a token's claims are valid. The logical extension of this method may be to extend the interface and support flagging erroneous claims, all the while maintaining a simple goal of validating and verifying a staked amount exists to benefit the operator experience. | ||
|
||
## Copyright | ||
Copyright and related rights waived via [CC0](../LICENSE.md). |