From fd54de525891148f411e8fd51d1589619317ea12 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 2 Nov 2021 10:15:05 -0400 Subject: [PATCH 01/12] fix(nft-payouts): Missing memo argument. --- specs/Standards/NonFungibleToken/Payout.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 829110b3d..3203b0b53 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -1,25 +1,30 @@ # Standard for a Multiple-Recipient-Payout mechanic on NFT Contracts (NEP-199) + Version `2.0.0`. This standard assumes the NFT contract has implemented [NEP-171](https://github.com/near/NEPs/blob/master/specs/Standards/NonFungibleToken/Core.md) (Core) and [NEP-178](https://github.com/near/NEPs/blob/master/specs/Standards/NonFungibleToken/ApprovalManagement.md) (Approval Management). ## Summary + An interface allowing non-fungible token contracts to request that financial contracts pay-out multiple receivers, enabling flexible royalty implementations. ## Motivation -Currently, NFTs on NEAR support the field `owner_id`, but lack flexibility for ownership and payout mechanics with more complexity, including but not limited to royalties. Financial contracts, such as marketplaces, auction houses, and NFT Loan contracts would benefit from a standard interface on NFT producer contracts for querying whom to pay out, and how much to pay. + +Currently, NFTs on NEAR support the field `owner_id`, but lack flexibility for ownership and payout mechanics with more complexity, including but not limited to royalties. Financial contracts, such as marketplaces, auction houses, and NFT loan contracts would benefit from a standard interface on NFT producer contracts for querying whom to pay out, and how much to pay. Therefore, the core goal of this standard is to define a set of methods for financial contracts to call, without specifying how NFT contracts define the divide of payout mechanics, and a standard `Payout` response structure. ## Guide-level explanation This Payout extension standard adds two methods to NFT contracts: + - a view method: `nft_payout`, accepting a `token_id` and some `balance`, returning the `Payout` mapping for the given token. - a call method: `nft_transfer_payout`, accepting all the arguments of`nft_transfer`, plus a field for some `Balance` that calculates the `Payout`, calls `nft_transfer`, and returns the `Payout` mapping. Financial contracts MUST validate several invariants on the returned `Payout`: + 1. The returned `Payout` MUST be no longer than the given maximum length (`max_len_payout` parameter) if provided. Payouts of excessive length can become prohibitively gas-expensive. Financial contracts can specify the maximum length of payout the contract is willing to respect with the `max_len_payout` field on `nft_transfer_payout`. 2. The balances MUST add up to less than or equal to the `balance` argument in `nft_transfer_payout`. If the balance adds up to less than the `balance` argument, the financial contract MAY claim the remainder for itself. 3. The sum of the balances MUST NOT overflow. This is technically identical to 2, but financial contracts should be expected to handle this possibility. @@ -32,6 +37,7 @@ If the Payout contains any addresses that do not exist, the financial contract M Financial contracts MAY take a cut of the NFT sale price as commission, subtracting their cut from the total token sale price, and calling `nft_transfer_payout` with the remainder. ## Example Flow + ``` ┌─────────────────────────────────────────────────┐ │Token Owner approves marketplace for token_id "0"│ @@ -54,6 +60,7 @@ Financial contracts MAY take a cut of the NFT sale price as commission, subtract ``` ## Reference-level explanation + ```rust /// A mapping of NEAR accounts to the amount each should be paid out, in /// the event of a token-sale. The payout mapping MUST be shorter than the @@ -66,7 +73,7 @@ pub struct Payout { pub payout: HashMap, } -pub trait Payouts{ +pub trait Payouts { /// Given a `token_id` and NEAR-denominated balance, return the `Payout`. /// struct for the given token. Panic if the length of the payout exceeds /// `max_len_payout.` @@ -80,12 +87,13 @@ pub trait Payouts{ receiver_id: AccountId, token_id: String, approval_id: u64, + memo: Option balance: U128, max_len_payout: u32, - ) -> Payout{ + ) -> Payout { assert_one_yocto(); let payout = self.nft_payout(token_id, balance); - self.nft_transfer(receiver_id, token_id, approval_id); + self.nft_transfer(receiver_id, token_id, approval_id, memo); payout } } @@ -104,4 +112,4 @@ In the future, the NFT contract itself may be able to place an NFT transfer is a ## Errata -Version `2.0.0` contains the intended `approval_id` of `u64` instead of the stringified `U64` version. This was an oversight, but since the standard was live for a few months before noticing, the team thought it best to bump the major version. \ No newline at end of file +Version `2.0.0` contains the intended `approval_id` of `u64` instead of the stringified `U64` version. This was an oversight, but since the standard was live for a few months before noticing, the team thought it best to bump the major version. From 1b97599b5a296122f645a749e6f5fa666fd970bc Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 2 Nov 2021 13:47:42 -0400 Subject: [PATCH 02/12] Update specs/Standards/NonFungibleToken/Payout.md --- specs/Standards/NonFungibleToken/Payout.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 3203b0b53..5afd168c3 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -87,7 +87,7 @@ pub trait Payouts { receiver_id: AccountId, token_id: String, approval_id: u64, - memo: Option + memo: Option, balance: U128, max_len_payout: u32, ) -> Payout { From fe3258e4c344112873194d7fcdd20224b4768a94 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 11 Nov 2021 16:32:59 -0500 Subject: [PATCH 03/12] feat(NFT): `payout` to hashmap & add option type to `max_len_payout`s This is to conform with Paras' initial NFT market place --- specs/Standards/NonFungibleToken/Payout.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 5afd168c3..25d8a7b38 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -67,17 +67,13 @@ Financial contracts MAY take a cut of the NFT sale price as commission, subtract /// maximum length specified by the financial contract obtaining this /// payout data. Any mapping of length 10 or less MUST be accepted by /// financial contracts, so 10 is a safe upper limit. -#[derive(Serialize, Deserialize)] -#[serde(crate = "near_sdk::serde")] -pub struct Payout { - pub payout: HashMap, -} +pub type payout = HashMap, pub trait Payouts { /// Given a `token_id` and NEAR-denominated balance, return the `Payout`. /// struct for the given token. Panic if the length of the payout exceeds /// `max_len_payout.` - fn nft_payout(&self, token_id: String, balance: U128, max_len_payout: u32) -> Payout; + fn nft_payout(&self, token_id: String, balance: U128, max_len_payout: Option) -> Payout; /// Given a `token_id` and NEAR-denominated balance, transfer the token /// and return the `Payout` struct for the given token. Panic if the /// length of the payout exceeds `max_len_payout.` @@ -89,7 +85,7 @@ pub trait Payouts { approval_id: u64, memo: Option, balance: U128, - max_len_payout: u32, + max_len_payout: Option, ) -> Payout { assert_one_yocto(); let payout = self.nft_payout(token_id, balance); From 0ea251b5cbdebc5d58b91840a936a3a941efd539 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Wed, 17 Nov 2021 11:08:59 -0500 Subject: [PATCH 04/12] fix: memo should be string --- specs/Standards/NonFungibleToken/Payout.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 25d8a7b38..0e2cf5734 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -83,7 +83,7 @@ pub trait Payouts { receiver_id: AccountId, token_id: String, approval_id: u64, - memo: Option, + memo: Option, balance: U128, max_len_payout: Option, ) -> Payout { From 591cba71e4478b210ed10c8ff84a48dcb332d869 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 14 Dec 2021 15:57:03 +0100 Subject: [PATCH 05/12] feat: add more notes about potential errors --- specs/Standards/NonFungibleToken/Payout.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 0e2cf5734..4eb07f169 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -95,7 +95,15 @@ pub trait Payouts { } ``` -Note that NFT and financial contracts will vary in implementation. This means that some extra CPU cycles may occur in one NFT contract and not another. Furthermore, a financial contract may accept fungible tokens, native NEAR, or another entity as payment. Transferring native NEAR tokens is less expensive in gas than sending fungible tokens. For these reasons, the maximum length of payouts may vary according to the customization of the smart contracts. +## Fallback on error + +In the case where either the `max_len_payout` causes a panic, or a malformed `Payout` is returned, the caller contract should transfer all funds to the token owner. + +## Potential pitfalls + +The payout must include all accounts that should receive funds. Thus the token owner must be included. + +NFT and financial contracts vary in implementation. This means that some extra CPU cycles may occur in one NFT contract and not another. Furthermore, a financial contract may accept fungible tokens, native NEAR, or another entity as payment. Transferring native NEAR tokens is less expensive in gas than sending fungible tokens. For these reasons, the maximum length of payouts may vary according to the customization of the smart contracts. ## Drawbacks From 145d0695e3e8823b9e03e1caf9d705e0f24582f9 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 14 Dec 2021 11:16:32 -0500 Subject: [PATCH 06/12] Apply suggestions from code review --- specs/Standards/NonFungibleToken/Payout.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 4eb07f169..4919ba0d1 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -97,11 +97,11 @@ pub trait Payouts { ## Fallback on error -In the case where either the `max_len_payout` causes a panic, or a malformed `Payout` is returned, the caller contract should transfer all funds to the token owner. +In the case where either the `max_len_payout` causes a panic, or a malformed `Payout` is returned, the caller contract should transfer all funds to the original token owner selling the token. ## Potential pitfalls -The payout must include all accounts that should receive funds. Thus the token owner must be included. +The payout must include all accounts that should receive funds. Thus it is a mistake to assume that the original token owner will receive funds if they are not included in the payout. NFT and financial contracts vary in implementation. This means that some extra CPU cycles may occur in one NFT contract and not another. Furthermore, a financial contract may accept fungible tokens, native NEAR, or another entity as payment. Transferring native NEAR tokens is less expensive in gas than sending fungible tokens. For these reasons, the maximum length of payouts may vary according to the customization of the smart contracts. From 5894c4042a9b54e8a801197dbdb4979d0ae70ed9 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 23 Dec 2021 12:34:22 +0100 Subject: [PATCH 07/12] fix: revert to Payout struct; remove payable decorator --- specs/Standards/NonFungibleToken/Payout.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 4919ba0d1..4da2f7a1e 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -67,7 +67,11 @@ Financial contracts MAY take a cut of the NFT sale price as commission, subtract /// maximum length specified by the financial contract obtaining this /// payout data. Any mapping of length 10 or less MUST be accepted by /// financial contracts, so 10 is a safe upper limit. -pub type payout = HashMap, +#[Derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct Payout { + payout: HashMap, +} pub trait Payouts { /// Given a `token_id` and NEAR-denominated balance, return the `Payout`. @@ -77,7 +81,6 @@ pub trait Payouts { /// Given a `token_id` and NEAR-denominated balance, transfer the token /// and return the `Payout` struct for the given token. Panic if the /// length of the payout exceeds `max_len_payout.` - #[payable] fn nft_transfer_payout( &mut self, receiver_id: AccountId, From eafefc65c11f10ec8cc783c0fcc9c97948362adf Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 13 Jan 2022 23:20:06 +0100 Subject: [PATCH 08/12] Apply suggestions from code review --- specs/Standards/NonFungibleToken/Payout.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 4da2f7a1e..30e19754d 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -1,6 +1,6 @@ # Standard for a Multiple-Recipient-Payout mechanic on NFT Contracts (NEP-199) -Version `2.0.0`. +Version `2.1.0`. This standard assumes the NFT contract has implemented [NEP-171](https://github.com/near/NEPs/blob/master/specs/Standards/NonFungibleToken/Core.md) (Core) and [NEP-178](https://github.com/near/NEPs/blob/master/specs/Standards/NonFungibleToken/ApprovalManagement.md) (Approval Management). @@ -67,7 +67,7 @@ Financial contracts MAY take a cut of the NFT sale price as commission, subtract /// maximum length specified by the financial contract obtaining this /// payout data. Any mapping of length 10 or less MUST be accepted by /// financial contracts, so 10 is a safe upper limit. -#[Derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(crate = "near_sdk::serde")] pub struct Payout { payout: HashMap, @@ -85,7 +85,7 @@ pub trait Payouts { &mut self, receiver_id: AccountId, token_id: String, - approval_id: u64, + approval_id: Option, memo: Option, balance: U128, max_len_payout: Option, From 0688ec75f1b8ffbc0a7dd5b5c24f27aaa86094b3 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 15 Feb 2022 18:47:10 +0100 Subject: [PATCH 09/12] Update specs/Standards/NonFungibleToken/Payout.md --- specs/Standards/NonFungibleToken/Payout.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 947228acc..fdc18eacd 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -121,4 +121,5 @@ In the future, the NFT contract itself may be able to place an NFT transfer is a ## Errata +- Version `2.1.0` adds a memo parameter to `nft_transfer_payout`, which previously forced implementers of `2.0.0` to pass `None` to the inner `nft_transfer`. Also refactors `max_len_payout` to be an option type. Version `2.0.0` contains the intended `approval_id` of `u64` instead of the stringified `U64` version. This was an oversight, but since the standard was live for a few months before noticing, the team thought it best to bump the major version. From 81ef38de548c5b293974fe1563f7d9af6f243836 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 15 Feb 2022 18:47:49 +0100 Subject: [PATCH 10/12] Update specs/Standards/NonFungibleToken/Payout.md --- specs/Standards/NonFungibleToken/Payout.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index fdc18eacd..908d35a6b 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -122,4 +122,4 @@ In the future, the NFT contract itself may be able to place an NFT transfer is a ## Errata - Version `2.1.0` adds a memo parameter to `nft_transfer_payout`, which previously forced implementers of `2.0.0` to pass `None` to the inner `nft_transfer`. Also refactors `max_len_payout` to be an option type. -Version `2.0.0` contains the intended `approval_id` of `u64` instead of the stringified `U64` version. This was an oversight, but since the standard was live for a few months before noticing, the team thought it best to bump the major version. +- Version `2.0.0` contains the intended `approval_id` of `u64` instead of the stringified `U64` version. This was an oversight, but since the standard was live for a few months before noticing, the team thought it best to bump the major version. From 6b7fe61d973612a4182b2bec06585e359840efb8 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 17 Nov 2022 11:01:40 -0500 Subject: [PATCH 11/12] Update specs/Standards/NonFungibleToken/Payout.md --- specs/Standards/NonFungibleToken/Payout.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/Standards/NonFungibleToken/Payout.md b/specs/Standards/NonFungibleToken/Payout.md index 908d35a6b..9d48c537f 100644 --- a/specs/Standards/NonFungibleToken/Payout.md +++ b/specs/Standards/NonFungibleToken/Payout.md @@ -83,6 +83,7 @@ pub trait Payouts { /// Given a `token_id` and NEAR-denominated balance, transfer the token /// and return the `Payout` struct for the given token. Panic if the /// length of the payout exceeds `max_len_payout.` + #[payable] fn nft_transfer_payout( &mut self, receiver_id: AccountId, From 18828873648eff1a2e8464db234aefd70918b3e0 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 17 Nov 2022 11:16:49 -0500 Subject: [PATCH 12/12] chore: move to nep-0199.md --- neps/nep-0199.md | 33 +++-- .../Tokens/NonFungibleToken/Payout.md | 129 ++++++++++++++++++ 2 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 specs/Standards/Tokens/NonFungibleToken/Payout.md diff --git a/neps/nep-0199.md b/neps/nep-0199.md index 92c51f4bf..0821fe357 100644 --- a/neps/nep-0199.md +++ b/neps/nep-0199.md @@ -16,7 +16,7 @@ An interface allowing non-fungible token contracts to request that financial con ## Motivation -Currently, NFTs on NEAR support the field `owner_id`, but lack flexibility for ownership and payout mechanics with more complexity, including but not limited to royalties. Financial contracts, such as marketplaces, auction houses, and NFT Loan contracts would benefit from a standard interface on NFT producer contracts for querying whom to pay out, and how much to pay. +Currently, NFTs on NEAR support the field `owner_id`, but lack flexibility for ownership and payout mechanics with more complexity, including but not limited to royalties. Financial contracts, such as marketplaces, auction houses, and NFT loan contracts would benefit from a standard interface on NFT producer contracts for querying whom to pay out, and how much to pay. Therefore, the core goal of this standard is to define a set of methods for financial contracts to call, without specifying how NFT contracts define the divide of payout mechanics, and a standard `Payout` response structure. @@ -76,11 +76,11 @@ pub struct Payout { pub payout: HashMap, } -pub trait Payouts{ +pub trait Payouts { /// Given a `token_id` and NEAR-denominated balance, return the `Payout`. /// struct for the given token. Panic if the length of the payout exceeds /// `max_len_payout.` - fn nft_payout(&self, token_id: String, balance: U128, max_len_payout: u32) -> Payout; + fn nft_payout(&self, token_id: String, balance: U128, max_len_payout: Option) -> Payout; /// Given a `token_id` and NEAR-denominated balance, transfer the token /// and return the `Payout` struct for the given token. Panic if the /// length of the payout exceeds `max_len_payout.` @@ -89,19 +89,33 @@ pub trait Payouts{ &mut self, receiver_id: AccountId, token_id: String, - approval_id: u64, + approval_id: Option, + memo: Option, balance: U128, - max_len_payout: u32, - ) -> Payout{ + max_len_payout: Option, + ) -> Payout { assert_one_yocto(); let payout = self.nft_payout(token_id, balance); - self.nft_transfer(receiver_id, token_id, approval_id); + self.nft_transfer(receiver_id, token_id, approval_id, memo); payout } } ``` -Note that NFT and financial contracts will vary in implementation. This means that some extra CPU cycles may occur in one NFT contract and not another. Furthermore, a financial contract may accept fungible tokens, native NEAR, or another entity as payment. Transferring native NEAR tokens is less expensive in gas than sending fungible tokens. For these reasons, the maximum length of payouts may vary according to the customization of the smart contracts. +## Fallback on error + +In the case where either the `max_len_payout` causes a panic, or a malformed `Payout` is returned, the caller contract should transfer all funds to the original token owner selling the token. + +## Potential pitfalls + +The payout must include all accounts that should receive funds. Thus it is a mistake to assume that the original token owner will receive funds if they are not included in the payout. + +NFT and financial contracts vary in implementation. This means that some extra CPU cycles may occur in one NFT contract and not another. Furthermore, a financial contract may accept fungible tokens, native NEAR, or another entity as payment. Transferring native NEAR tokens is less expensive in gas than sending fungible tokens. For these reasons, the maximum length of payouts may vary according to the customization of the smart contracts. + +## Drawbacks + +There is an introduction of trust that the contract calling `nft_transfer_payout` will indeed pay out to all intended parties. However, since the calling contract will typically be something like a marketplace used by end users, malicious actors might be found out more easily and might have less incentive. +There is an assumption that NFT contracts will understand the limits of gas and not allow for a number of payouts that cannot be achieved. ## Future possibilities @@ -109,7 +123,8 @@ In the future, the NFT contract itself may be able to place an NFT transfer is a ## Errata -Version `2.0.0` contains the intended `approval_id` of `u64` instead of the stringified `U64` version. This was an oversight, but since the standard was live for a few months before noticing, the team thought it best to bump the major version. +- Version `2.1.0` adds a memo parameter to `nft_transfer_payout`, which previously forced implementers of `2.0.0` to pass `None` to the inner `nft_transfer`. Also refactors `max_len_payout` to be an option type. +- Version `2.0.0` contains the intended `approval_id` of `u64` instead of the stringified `U64` version. This was an oversight, but since the standard was live for a few months before noticing, the team thought it best to bump the major version. ## Copyright [copyright]: #copyright diff --git a/specs/Standards/Tokens/NonFungibleToken/Payout.md b/specs/Standards/Tokens/NonFungibleToken/Payout.md new file mode 100644 index 000000000..2464eb5b1 --- /dev/null +++ b/specs/Standards/Tokens/NonFungibleToken/Payout.md @@ -0,0 +1,129 @@ +# Royalties and Payouts + +## [NEP-199](https://github.com/near/NEPs/blob/master/neps/nep-0199.md) + +Version `2.0.0`. + +This standard assumes the NFT contract has implemented +[NEP-171](https://github.com/near/NEPs/blob/master/specs/Standards/Tokens/NonFungibleToken/Core.md) (Core) and [NEP-178](https://github.com/near/NEPs/blob/master/specs/Standards/Tokens/NonFungibleToken/ApprovalManagement.md) (Approval Management). + +## Summary + +An interface allowing non-fungible token contracts to request that financial contracts pay-out multiple receivers, enabling flexible royalty implementations. + +## Motivation + +Currently, NFTs on NEAR support the field `owner_id`, but lack flexibility for ownership and payout mechanics with more complexity, including but not limited to royalties. Financial contracts, such as marketplaces, auction houses, and NFT loan contracts would benefit from a standard interface on NFT producer contracts for querying whom to pay out, and how much to pay. + +Therefore, the core goal of this standard is to define a set of methods for financial contracts to call, without specifying how NFT contracts define the divide of payout mechanics, and a standard `Payout` response structure. + + +## Specification + +This Payout extension standard adds two methods to NFT contracts: +- a view method: `nft_payout`, accepting a `token_id` and some `balance`, returning the `Payout` mapping for the given token. +- a call method: `nft_transfer_payout`, accepting all the arguments of`nft_transfer`, plus a field for some `Balance` that calculates the `Payout`, calls `nft_transfer`, and returns the `Payout` mapping. + +Financial contracts MUST validate several invariants on the returned +`Payout`: +1. The returned `Payout` MUST be no longer than the given maximum length (`max_len_payout` parameter) if provided. Payouts of excessive length can become prohibitively gas-expensive. Financial contracts can specify the maximum length of payout the contract is willing to respect with the `max_len_payout` field on `nft_transfer_payout`. +2. The balances MUST add up to less than or equal to the `balance` argument in `nft_transfer_payout`. If the balance adds up to less than the `balance` argument, the financial contract MAY claim the remainder for itself. +3. The sum of the balances MUST NOT overflow. This is technically identical to 2, but financial contracts should be expected to handle this possibility. + +Financial contracts MAY specify their own maximum length payout to respect. +At minimum, financial contracts MUST NOT set their maximum length below 10. + +If the Payout contains any addresses that do not exist, the financial contract MAY keep those wasted payout funds. + +Financial contracts MAY take a cut of the NFT sale price as commission, subtracting their cut from the total token sale price, and calling `nft_transfer_payout` with the remainder. + +## Example Flow +``` + ┌─────────────────────────────────────────────────┐ + │Token Owner approves marketplace for token_id "0"│ + ├─────────────────────────────────────────────────┘ + │ nft_approve("0",market.near,) + ▼ + ┌───────────────────────────────────────────────┐ + │Marketplace sells token to user.near for 10N │ + ├───────────────────────────────────────────────┘ + │ nft_transfer_payout(user.near,"0",0,"10000000",5) + ▼ + ┌───────────────────────────────────────────────┐ + │NFT contract returns Payout data │ + ├───────────────────────────────────────────────┘ + │ Payout(, +} + +pub trait Payouts { + /// Given a `token_id` and NEAR-denominated balance, return the `Payout`. + /// struct for the given token. Panic if the length of the payout exceeds + /// `max_len_payout.` + fn nft_payout(&self, token_id: String, balance: U128, max_len_payout: Option) -> Payout; + /// Given a `token_id` and NEAR-denominated balance, transfer the token + /// and return the `Payout` struct for the given token. Panic if the + /// length of the payout exceeds `max_len_payout.` + #[payable] + fn nft_transfer_payout( + &mut self, + receiver_id: AccountId, + token_id: String, + approval_id: Option, + memo: Option, + balance: U128, + max_len_payout: Option, + ) -> Payout { + assert_one_yocto(); + let payout = self.nft_payout(token_id, balance); + self.nft_transfer(receiver_id, token_id, approval_id, memo); + payout + } +} +``` + +## Fallback on error + +In the case where either the `max_len_payout` causes a panic, or a malformed `Payout` is returned, the caller contract should transfer all funds to the original token owner selling the token. + +## Potential pitfalls + +The payout must include all accounts that should receive funds. Thus it is a mistake to assume that the original token owner will receive funds if they are not included in the payout. + +NFT and financial contracts vary in implementation. This means that some extra CPU cycles may occur in one NFT contract and not another. Furthermore, a financial contract may accept fungible tokens, native NEAR, or another entity as payment. Transferring native NEAR tokens is less expensive in gas than sending fungible tokens. For these reasons, the maximum length of payouts may vary according to the customization of the smart contracts. + +## Drawbacks + +There is an introduction of trust that the contract calling `nft_transfer_payout` will indeed pay out to all intended parties. However, since the calling contract will typically be something like a marketplace used by end users, malicious actors might be found out more easily and might have less incentive. +There is an assumption that NFT contracts will understand the limits of gas and not allow for a number of payouts that cannot be achieved. + +## Future possibilities + +In the future, the NFT contract itself may be able to place an NFT transfer is a state that is "pending transfer" until all payouts have been awarded. This would keep all the information inside the NFT and remove trust. + +## Errata + +- Version `2.1.0` adds a memo parameter to `nft_transfer_payout`, which previously forced implementers of `2.0.0` to pass `None` to the inner `nft_transfer`. Also refactors `max_len_payout` to be an option type. +- Version `2.0.0` contains the intended `approval_id` of `u64` instead of the stringified `U64` version. This was an oversight, but since the standard was live for a few months before noticing, the team thought it best to bump the major version. + +## Copyright +[copyright]: #copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). \ No newline at end of file