Replies: 3 comments 1 reply
-
Hello, this core Non-Fungible Token standard link seems broken. |
Beta Was this translation helpful? Give feedback.
1 reply
-
Rust API: /// Non-Fungible Token Approval NEP 178. Ref:
/// https://github.com/near/NEPs/blob/master/specs/Standards/NonFungibleToken/ApprovalManagement.md
pub trait NonFungibleContractApproval {
/// Add an approved account for a specific token.
///
/// Requirements
/// * Caller of the method must attach a deposit of at least 1 yoctoⓃ for
/// security purposes
/// * Contract MAY require caller to attach larger deposit, to cover cost of
/// storing approver data
/// * Contract MUST panic if called by someone other than token owner
/// * Contract MUST panic if addition would cause `nft_revoke_all` to exceed
/// single-block gas limit
/// * Contract MUST increment approval ID even if re-approving an account
/// * If successfully approved or if had already been approved, and if `msg` is
/// present, contract MUST call `nft_on_approve` on `account_id`. See
/// `nft_on_approve` description below for details.
///
/// Arguments:
/// * `token_id`: the token for which to add an approval
/// * `account_id`: the account to add to `approvals`
/// * `msg`: optional String to be passed to `nft_on_approve`
///
/// Returns void, if no `msg` given. Otherwise, returns promise call to
/// `nft_on_approve`, which can resolve with whatever it wants.
fn nft_approve(
&mut self,
token_id: TokenId,
account_id: AccountId,
msg: Option<String>,
) -> Option<Promise>;
/// Revoke an approved account for a specific token.
///
/// Requirements
/// * Caller of the method must attach a deposit of 1 yoctoⓃ for security
/// purposes
/// * If contract requires >1yN deposit on `nft_approve`, contract
/// MUST refund associated storage deposit when owner revokes approval
/// * Contract MUST panic if called by someone other than token owner
///
/// Arguments:
/// * `token_id`: the token for which to revoke an approval
/// * `account_id`: the account to remove from `approvals`
fn nft_revoke(&mut self, token_id: String, account_id: AccountId);
/// Revoke all approved accounts for a specific token.
///
/// Requirements
/// * Caller of the method must attach a deposit of 1 yoctoⓃ for security
/// purposes
/// * If contract requires >1yN deposit on `nft_approve`, contract
/// MUST refund all associated storage deposit when owner revokes approvals
/// * Contract MUST panic if called by someone other than token owner
///
/// Arguments:
/// * `token_id`: the token with approvals to revoke
fn nft_revoke_all(&mut self, token_id: String);
/// Check if a token is approved for transfer by a given account, optionally
/// checking an approval_id
///
/// Arguments:
/// * `token_id`: the token for which to revoke an approval
/// * `approved_account_id`: the account to check the existence of in `approvals`
/// * `approval_id`: an optional approval ID to check against current approval ID for given account
///
/// Returns:
/// if `approval_id` given, `true` if `approved_account_id` is approved with given `approval_id`
/// otherwise, `true` if `approved_account_id` is in list of approved accounts
fn nft_is_approved(
&self,
token_id: String,
approved_account_id: AccountId,
approval_id: Option<U64>,
) -> bool;
}
/// Non-Fungible Token Approval NEP 178. Ref:
/// https://github.com/near/NEPs/blob/master/specs/Standards/NonFungibleToken/ApprovalManagement.md
pub trait NonFungibleOnApprove {
/// Approved Account Contract Interface
/// If a contract that gets approved to transfer NFTs wants to, it can implement nft_on_approve to update its own state when granted approval for a token:
/// Respond to notification that contract has been granted approval for a token.
///
/// Notes
/// * Contract knows the token contract ID from `predecessor_account_id`
///
/// Arguments:
/// * `token_id`: the token to which this contract has been granted approval
/// * `owner_id`: the owner of the token
/// * `approval_id`: the approval ID stored by NFT contract for this approval.
/// Expected to be a number within the 2^53 limit representable by JSON.
/// * `msg`: specifies information needed by the approved contract in order to
/// handle the approval. Can indicate both a fn to call and the
/// parameters to pass to that fn.
fn nft_on_approve(
&mut self,
token_id: TokenId,
owner_id: AccountId,
approval_id: U64,
msg: String,
);
} |
Beta Was this translation helpful? Give feedback.
0 replies
-
Two comments, submitting in a pull request.
an implementation: #[near_bindgen]
impl NonFungibleContractApproval for MintbaseStore {
fn nft_approve(
&mut self,
token_id: String,
account_id: AccountId,
msg: Option<String>,
) -> Option<Promise> {
// assert_one_yocto(); // or more
let mut token = self
.nft_token(token_id.to_string())
.unwrap_or_else(|| env::panic(b"no token"));
assert_eq!(token.owner_id, env::predecessor_account_id());
let approval_id = self.list_nonce;
self.list_nonce += 1;
let token_owner_id = token.owner_id.to_string();
token.approvals.insert(account_id.to_string(), approval_id);
self
.tokens
.insert(&token_id.parse::<u64>().unwrap(), &token);
// log
if let Some(msg) = msg {
ext_on_approve::nft_on_approve(
token_id,
token_owner_id,
approval_id.into(),
msg,
&account_id,
NO_DEPOSIT,
DEFAULT_GAS,
)
.into()
} else {
None
}
}
fn nft_revoke(&mut self, token_id: String, account_id: AccountId) {
// assert_one_yocto();
// refund associated storage if applicable
let mut token = self.nft_token(token_id.to_string()).expect("no token");
assert_eq!(token.owner_id, env::predecessor_account_id());
token.approvals.remove(&account_id);
self
.tokens
.insert(&token_id.parse::<u64>().unwrap(), &token);
// log
}
fn nft_revoke_all(&mut self, token_id: String) {
// assert_one_yocto();
// refund associated storage if applicable
let mut token = self.nft_token(token_id.to_string()).expect("no token");
assert_eq!(token.owner_id, env::predecessor_account_id());
token.approvals.clear();
self
.tokens
.insert(&token_id.parse::<u64>().unwrap(), &token);
// log
}
fn nft_is_approved(
&self,
token_id: String,
approved_account_id: AccountId,
approval_id: Option<U64>,
) -> bool {
let token = self.nft_token(token_id).expect("no token");
if approved_account_id == token.owner_id {
true
} else {
let approval_id = approval_id.expect("approval_id required");
let stored_approval = token.approvals.get(&approved_account_id);
match stored_approval {
None => false,
Some(stored_approval_id) => *stored_approval_id == approval_id.0,
}
}
}
} |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
This discussion split from the more general discussion in #171. While that was intended to tackle the entire upcoming NFT standard, this discussion aims to only clarify the approval management aspect. Below seems to be the current best-guess at what a v1.0.0 standard might look like, based on current discussion in #171:
Summary
A system for allowing a set of users or contracts to transfer specific Non-Fungible Tokens on behalf of an owner. Similar to approval management systems in standards like ERC-721.
Motivation
People familiar with ERC-721 may expect to need an approval management system for basic transfers, where a simple transfer from Alice to Bob requires that Alice first approve Bob to spend one of her tokens, after which Bob can call
transfer_from
to actually transfer the token to himself.NEAR's core Non-Fungible Token standard includes good support for safe atomic transfers without such complexity. It even provides "transfer and call" functionality (
nft_transfer_call
) which allows a specific token to be "attached" to a call to a separate contract. For many Non-Fungible Token workflows, these options may circumvent the need for a full-blown Approval Managament system.However, some Non-Fungible Token developers, marketplaces, dApps, or artists may require greater control. This standard provides a uniform interface allowing token owners to approve other NEAR accounts, whether individuals or contracts, to transfer specific tokens on the owner's behalf.
Prior art:
Example Scenarios
Let's consider some examples. Our cast of characters & apps:
alice
with no contract deployed to itbob
with no contract deployed to itnft
, implementing only the Core NFT standard with this Approval Management extensionmarket
which sells tokens fromnft
as well as other NFT contractsnft_on_approve
function!), has accountbazaar
Alice and Bob are already registered with NFT, Market, and Bazaar, and Alice owns a token on the NFT contract with ID=
"1"
.Let's examine the technical calls through the following scenarios:
msg
so that NFT will callnft_on_approve
on Market's contract.msg
again, but what's this? Bazaar doesn't implementnft_on_approve
, so Alice sees an error in the transaction result. Not to worry, though, she checksnft_is_approved
and sees that she did successfully approve Bazaar, despite the error.1. Simple Approval
Alice approves Bob to transfer her token.
High-level explanation
Technical calls
Alice calls
nft::nft_approve({ "token_id": "1", "account_id": "bob" })
. She attaches 1 yoctoⓃ, (.000000000000000000000001Ⓝ). Using NEAR CLI to make this call, the command would be:The response:
Alice calls view method
nft_token
:The response:
Alice calls view method
nft_is_approved
:The response:
2. Approval with cross-contract call
Alice approves Market to transfer one of her tokens and passes
msg
so that NFT will callnft_on_approve
on Market's contract. She probably does this via Market's frontend app which would know how to constructmsg
in a useful way.High-level explanation
nft_approve
to approvemarket
to transfer her token, and passes amsg
msg
is included,nft
will schedule a cross-contract call tomarket
nft_approve
call.Technical calls
Using near-cli:
At this point, near-cli will hang until the cross-contract call chain fully resolves, which would also be true if Alice used a Market frontend using near-api-js. Alice's part is done, though. The rest happens behind the scenes.
nft
schedules a call tonft_on_approve
onmarket
. Using near-cli notation for easy cross-reference with the above, this would look like:market
now knows that it can sell Alice's token for 100 nDAI, and that when it transfers it to a buyer usingnft_transfer
, it can pass along the givenapproval_id
to ensure that Alice hasn't changed her mind. It can schedule any further cross-contract calls it wants, and if it returns these promises correctly, Alice's initial near-cli call will resolve with the outcome from the final step in the chain. If Alice actually made this call from a Market frontend, the frontend can use this return value for something useful.3. Approval with cross-contract call, edge case
Alice approves Bazaar and passes
msg
again. Maybe she actually does this via near-cli, rather than using Bazaar's frontend, because what's this? Bazaar doesn't implementnft_on_approve
, so Alice sees an error in the transaction result.Not to worry, though, she checks
nft_is_approved
and sees that she did successfully approve Bazaar, despite the error. She will have to find a new way to list her token for sale in Bazaar, rather than using the samemsg
shortcut that worked for Market.High-level explanation
nft_approve
to approvebazaar
to transfer her token, and passes amsg
.msg
is included,nft
will schedule a cross-contract call tobazaar
.nft_on_approve
, so this call results in an error. The approval still worked, but Alice sees an error in her near-cli output.bazaar
is approved, and sees that it is, despite the error.Technical calls
Using near-cli:
nft
schedules a call tonft_on_approve
onmarket
. Using near-cli notation for easy cross-reference with the above, this would look like:💥
bazaar
doesn't implement this method, so the call results in an error. Alice sees this error in the output from near-cli.Alice checks if the approval itself worked, despite the error on the cross-contract call:
The response:
4. Approval IDs
Bob buys Alice's token via Market. Bob probably does this via Market's frontend, which will probably initiate the transfer via a call to
ft_transfer_call
on the nDAI contract to transfer 100 nDAI tomarket
. Like the NFT standard's "transfer and call" function, Fungible Token'sft_transfer_call
takes amsg
whichmarket
can use to pass along information it will need to pay Alice and actually transfer the NFT. The actual transfer of the NFT is the only part we care about here.High-level explanation
market
contract callingnft_transfer
on thenft
contract, as described above. To be trustworthy and pass security audits,market
needs to pass alongapproval_id
so that it knows it has up-to-date information.Technical calls
Using near-cli notation for consistency:
5. Approval IDs, edge case
Bob transfers same token back to Alice, Alice re-approves Market & Bazaar, listing her token at a higher price than before. Bazaar is somehow unaware of these changes, and still stores
approval_id: 3
internally along with Alice's old price. Bob tries to buy from Bazaar at the old price. Like the previous example, this probably starts with a call to a different contract, which eventually results in a call tonft_transfer
onbazaar
. Let's consider a possible scenario from that point.High-level explanation
Bob signs some transaction which results in the
bazaar
contract callingnft_transfer
on thenft
contract, as described above. To be trustworthy and pass security audits,bazaar
needs to pass alongapproval_id
so that it knows it has up-to-date information. It does not have up-to-date information, so the call fails. If the initialnft_transfer
call is part of a call chain originating from a call toft_transfer_call
on a fungible token, Bob's payment will be refunded and no assets will change hands.Technical calls
Using near-cli notation for consistency:
6. Revoke one
Alice revokes Market's approval for this token.
Technical calls
Using near-cli:
Note that
market
will not get a cross-contract call in this case. The implementors of the Market app should implement cron-type functionality to intermittently check that Market still has the access they expect.7. Revoke all
Alice revokes all approval for this token.
Technical calls
Using near-cli:
Again, note that no previous approvers will get cross-contract calls in this case.
Reference-level explanation
The
Token
structure returned bynft_token
must include anapprovals
field, which is a map of account IDs to approval IDs. Using TypeScript's Record type notation:type Token = { id: string, owner_id: string, + approvals: Record<string, number>, };
Example token data:
What is an "approval ID"?
This is a unique number given to each approval that allows well-intentioned marketplaces or other 3rd-party NFT resellers to avoid a race condition. The race condition occurs when:
Note that while this describes an honest mistake, the possibility of such a bug can also be taken advantage of by malicious parties via front-running.
To avoid this possibility, the NFT contract generates a unique approval ID each time it approves an account. Then when calling
nft_transfer
ornft_transfer_call
, the approved account passesapproval_id
with this value to make sure the underlying state of the token hasn't changed from what the approved account expects.Keeping with the example above, say the initial approval of the second marketplace generated the following
approvals
data:But after the transfers and re-approval described above, the token might have
approvals
as:The marketplace then tries to call
nft_transfer
, passing outdated information:Interface
The NFT contract must implement the following methods:
Approved Account Contract Interface
If a contract that gets approved to transfer NFTs wants to, it can implement
nft_on_approve
to update its own state when granted approval for a token:Note that the NFT contract will fire-and-forget this call, ignoring any return values or errors generated. This means that even if the approved account does not have a contract or does not implement
nft_on_approve
, the approval will still work correctly from the point of view of the NFT contract.Further note that there is no parallel
nft_on_revoke
when revoking either a single approval or when revoking all. This is partially because scheduling manynft_on_revoke
calls when revoking all approvals could incur prohibitive gas fees. Apps and contracts which cache NFT approvals can therefore not rely on having up-to-date information, and should periodically refresh their caches. Since this will be the necessary reality for dealing withnft_revoke_all
, there is no reason to complicatenft_revoke
with annft_on_revoke
call.No incurred cost for core NFT behavior
NFT contracts should be implemented in a way to avoid extra gas fees for serialization & deserialization of
approvals
for calls tonft_*
methods other thannft_token
. Seenear-contract-standards
implementation offt_metadata
usingLazyOption
as a reference example.Beta Was this translation helpful? Give feedback.
All reactions