From 8cf5fa2f0a9e44d0a2d70c34d7f990f6f98e22ef Mon Sep 17 00:00:00 2001 From: chuwwwi Date: Sat, 31 Jul 2021 16:16:27 -0400 Subject: [PATCH] Added support for multiple royalty payments for FT tokens Added new types and functions for multiple royalty payments (Alice gets 10%, Bob gets 5%) in new token and manager files. These files are built off `fa2_multi_ft_token` and `fa2_multi_ft_token_manager` (they're mostly just copies of the original files with some new functionality added). - Added new data types that allow each `token_id` to have its own royalty scheme, an `(address, nat) map` that maps the royalty recipient to the percent of the transaction - Added a new entrypoint `Royalty_transfer` that supports tez payments in (batch) transfers of FT tokens; the user/caller of a batch royalty transfer will specify the amount of tez in each individual transfer - Added function to get and store the royalty scheme passed in by the user/admin upon token creation and to verify that the scheme is valid (sum of `nat`s adds up to the specified `total`) - Added checks to prevent users from attempting to batch transfer tez out of the contract by specifying more tez than is sent with the contract call --- ...multi_ft_token_manager_multi_royalty.mligo | 136 +++++++++++++ .../ft/fa2_multi_ft_token_multi_royalty.mligo | 192 ++++++++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 packages/minter-contracts/ligo/src/minter_collection/ft/fa2_multi_ft_token_manager_multi_royalty.mligo create mode 100644 packages/minter-contracts/ligo/src/minter_collection/ft/fa2_multi_ft_token_multi_royalty.mligo diff --git a/packages/minter-contracts/ligo/src/minter_collection/ft/fa2_multi_ft_token_manager_multi_royalty.mligo b/packages/minter-contracts/ligo/src/minter_collection/ft/fa2_multi_ft_token_manager_multi_royalty.mligo new file mode 100644 index 00000000..1949cafc --- /dev/null +++ b/packages/minter-contracts/ligo/src/minter_collection/ft/fa2_multi_ft_token_manager_multi_royalty.mligo @@ -0,0 +1,136 @@ +(* + One of the possible implementations of token management API which can create + new fungible tokens, mint and burn them. + + Token manager API allows to: + + 1. Create new toke types, + 2. Mint and burn tokens to some existing or new owner account. + + Burn operation fails if the owner holds less tokens then burn amount. +*) + +#if !TOKEN_MANAGER +#define TOKEN_MANAGER + +#include "fa2_multi_ft_token.mligo" + +type mint_burn_tx = +[@layout:comb] +{ + owner : address; + token_id : token_id; + amount : nat; +} + +type mint_burn_tokens_param = mint_burn_tx list + + +(* `token_manager` entry points *) +type token_manager = + | Create_token of (token_metadata * royalty) + | Mint_tokens of mint_burn_tokens_param + | Burn_tokens of mint_burn_tokens_param + +let verify_royalty (ry : royalty) : unit = + let sum_values = (fun (acc, entry : nat * (address * nat)) -> acc + entry.1) in + let expected_total = Map.fold sum_values royalty.scheme 0 in + if expected_total <> royalty.total then (failwith "Invalid total!" : unit) + else (() : unit) + +let create_token (metadata, royalty, storage + : token_metadata * royalty * multi_ft_token_storage) : multi_ft_token_storage = + (* extract token id *) + let new_token_id = metadata.token_id in + let existing_meta = Big_map.find_opt new_token_id storage.token_metadata in + match existing_meta with + | Some m -> (failwith "FA2_DUP_TOKEN_ID" : multi_ft_token_storage) + | None -> + let verify_royalty (royalty) in + let meta = Big_map.add new_token_id metadata storage.token_metadata in + let supply = Big_map.add new_token_id 0n storage.token_total_supply in + let new_rates = Big_map.add new_token_id royalty storage.rates in + { storage with + token_metadata = meta; + token_total_supply = supply; + rates = new_rates; + } + + +let mint_update_balances (txs, ledger : (mint_burn_tx list) * ledger) : ledger = + let mint = fun (l, tx : ledger * mint_burn_tx) -> + inc_balance (tx.owner, tx.token_id, tx.amount, l) in + + List.fold mint txs ledger + +let mint_update_total_supply (txs, total_supplies + : (mint_burn_tx list) * token_total_supply) : token_total_supply = + let update = fun (supplies, tx : token_total_supply * mint_burn_tx) -> + let supply_opt = Big_map.find_opt tx.token_id supplies in + match supply_opt with + | None -> (failwith fa2_token_undefined : token_total_supply) + | Some ts -> + let new_s = ts + tx.amount in + Big_map.update tx.token_id (Some new_s) supplies in + + List.fold update txs total_supplies + +let mint_tokens (param, storage : mint_burn_tokens_param * multi_ft_token_storage) + : multi_ft_token_storage = + let new_ledger = mint_update_balances (param, storage.ledger) in + let new_supply = mint_update_total_supply (param, storage.token_total_supply) in + let new_s = { storage with + ledger = new_ledger; + token_total_supply = new_supply; + } in + new_s + +let burn_update_balances(txs, ledger : (mint_burn_tx list) * ledger) : ledger = + let burn = fun (l, tx : ledger * mint_burn_tx) -> + dec_balance (tx.owner, tx.token_id, tx.amount, l) in + + List.fold burn txs ledger + +let burn_update_total_supply (txs, total_supplies + : (mint_burn_tx list) * token_total_supply) : token_total_supply = + let update = fun (supplies, tx : token_total_supply * mint_burn_tx) -> + let supply_opt = Big_map.find_opt tx.token_id supplies in + match supply_opt with + | None -> (failwith fa2_token_undefined : token_total_supply) + | Some ts -> + let new_s = match Michelson.is_nat (ts - tx.amount) with + | None -> (failwith fa2_insufficient_balance : nat) + | Some s -> s + in + Big_map.update tx.token_id (Some new_s) supplies in + + List.fold update txs total_supplies + +let burn_tokens (param, storage : mint_burn_tokens_param * multi_ft_token_storage) + : multi_ft_token_storage = + + let new_ledger = burn_update_balances (param, storage.ledger) in + let new_supply = burn_update_total_supply (param, storage.token_total_supply) in + let new_s = { storage with + ledger = new_ledger; + token_total_supply = new_supply; + } in + new_s + +let ft_token_manager (param, s : token_manager * multi_ft_token_storage) + : (operation list) * multi_ft_token_storage = + match param with + + | Create_token {token_metadata, royalty} -> + let new_s = create_token (token_metadata, royalty, s) in + (([]: operation list), new_s) + + | Mint_tokens param -> + let new_s = mint_tokens (param, s) in + ([] : operation list), new_s + + | Burn_tokens param -> + let new_s = burn_tokens (param, s) in + ([] : operation list), new_s + +#endif diff --git a/packages/minter-contracts/ligo/src/minter_collection/ft/fa2_multi_ft_token_multi_royalty.mligo b/packages/minter-contracts/ligo/src/minter_collection/ft/fa2_multi_ft_token_multi_royalty.mligo new file mode 100644 index 00000000..d2b95e57 --- /dev/null +++ b/packages/minter-contracts/ligo/src/minter_collection/ft/fa2_multi_ft_token_multi_royalty.mligo @@ -0,0 +1,192 @@ +#if !FA2_MAC_TOKEN +#define FA2_MAC_TOKEN + +#include "../../../fa2/fa2_interface.mligo" +#include "../../../fa2/fa2_errors.mligo" +#include "../../../fa2/lib/fa2_operator_lib.mligo" + +type royalty_transfer = { + tez : tez; + transfer : transfer; +} + +type fa2_entry_points = + | Transfer of transfer list + | Balance_of of balance_of_param + | Update_operators of update_operator list + | Royalty_transfer of royalty_transfer list + (* | Token_metadata_registry of address contract *) + +(* (owner,token_id) -> balance *) +type ledger = ((address * token_id), nat) big_map + +(* token_id -> total_supply *) +type token_total_supply = (token_id, nat) big_map + +type royalty = { + scheme : (address, nat) map; + total : nat; +} + +type rates = (token_id, royalty) big_map + +#if !LIMITED_TOKEN_MANAGER + +type multi_ft_token_storage = { + ledger : ledger; + operators : operator_storage; + token_total_supply : token_total_supply; + token_metadata : token_metadata_storage; + rates : rates; +} + +#else + +type multi_ft_token_storage = { + ledger : ledger; + operators : operator_storage; + token_total_supply : token_total_supply; + token_metadata : token_metadata_storage; + next_token_id : token_id; + rates : rates; +} + +#endif + +(** +Ensures sum of (tez associated with each transfer) do not exceed Tezos.amount. + *) +let verify_royalty_transfer_list (rts : royalty_transfer list) : unit = + let amt : tez = Tezos.amount in + let sum (acc, rt : tez * royalty_transfer) : tez = acc + rt.tez in + let tez_sum = List.fold_left sum 0tez rts in + if tez_sum <= amt then () : unit + else (failwith "List of tez to associate w/ each transfer not valid!" : unit) + +let get_balance_amt (key, ledger : (address * nat) * ledger) : nat = + let bal_opt = Big_map.find_opt key ledger in + match bal_opt with + | None -> 0n + | Some b -> b + +let inc_balance (owner, token_id, amt, ledger + : address * token_id * nat * ledger) : ledger = + let key = owner, token_id in + let bal = get_balance_amt (key, ledger) in + let updated_bal = bal + amt in + if updated_bal = 0n + then Big_map.remove key ledger + else Big_map.update key (Some updated_bal) ledger + +let dec_balance (owner, token_id, amt, ledger + : address * token_id * nat * ledger) : ledger = + let key = owner, token_id in + let bal = get_balance_amt (key, ledger) in + match Michelson.is_nat (bal - amt) with + | None -> (failwith fa2_insufficient_balance : ledger) + | Some new_bal -> + if new_bal = 0n + then Big_map.remove key ledger + else Big_map.update key (Some new_bal) ledger + +(** +Update leger balances according to the specified transfers. Fails if any of the +permissions or constraints are violated. +@param txs transfers to be applied to the ledger +@param validate_op function that validates of the tokens from the particular owner can be transferred. + *) +let transfer (txs, validate_op, storage + : (transfer list) * operator_validator * multi_ft_token_storage) + : ledger = + let make_transfer = fun (l, tx : ledger * transfer) -> + List.fold + (fun (ll, dst : ledger * transfer_destination) -> + if not Big_map.mem dst.token_id storage.token_metadata + then (failwith fa2_token_undefined : ledger) + else + let u = validate_op (tx.from_, Tezos.sender, dst.token_id, storage.operators) in + let lll = dec_balance (tx.from_, dst.token_id, dst.amount, ll) in + inc_balance(dst.to_, dst.token_id, dst.amount, lll) + ) tx.txs l + in + List.fold make_transfer txs storage.ledger + +let royalty_transfer (rts, validate_op, storage + : (royalty_transfer list) * operator_validator * multi_ft_token_storage) + : operation list * multi_ft_token_storage = + let oplist_concat = (fun (left, right : operation list * operation list) -> // want some way to concat op lists + let (concat = fun (acc, op : operation list, operation) -> acc :: op) in + List.fold_left concat left right) in + let token_payments = (fun (tok_id, amt, current_owner: token_id, tez, address) -> // create a list of ops for the payments + let scheme : (address, nat) map = (Big_map.find_opt tok_id storage.rates).scheme in + let total : nat = (Big_map.find_opt tok_id storage.rates).total in + let get_payment_op = (fun (acc, entry : op list * (address, nat)) -> // to itereate over the entries and add the op of each + (if entry.0 = Tezos.self_address then + let op = Tezos.transaction (() : unit) ((entry.1 * amt) / total) current_owner + else let op = Tezos.trasnaction (() : unit) ((entry.1 * amt) / total) entry.0) in + acc :: op + ) in + Map.fold get_payment_op scheme ([] : operation list) + ) in + verify_royalty_transfer_list (rts) in // rts must be valid + let make_transfer = fun (l, rt : ledger * royalty_transfer) -> + List.fold + (fun (ll, dst : ledger * transfer_destination) -> + if not Big_map.mem dst.token_id storage.token_metadata + then (failwith fa2_token_undefined : ledger) + else + let u = validate_op (rt.transfer.from_, Tezos.sender, dst.token_id, storage.operators) in + let lll = dec_balance (rt.transfer.from_, dst.token_id, dst.amount, ll) in + inc_balance(dst.to_, dst.token_id, dst.amount, lll) in + + ) rt.transfer.txs l + in + let make_payment = fun (ops, rt : operation list * royalty_transfer) -> + List.fold + (fun (opss, dst : operation list * transfer_destination) -> + oplist_concat opss (token_payments (dst.token_id, rt.tez, rt.transfer.from_)) + ) rt.transfer.txs ops + let new_storage : multi_ft_token_storage = { storage with ledger = List.fold make_transfer rts storage.ledger; } in + let ops : operation list = List.fold make_payment rts ([] : operation list) in + (ops, new_storage) + +let get_balance (p, ledger, tokens + : balance_of_param * ledger * token_metadata_storage) : operation = + let to_balance = fun (r : balance_of_request) -> + if not Big_map.mem r.token_id tokens + then (failwith fa2_token_undefined : balance_of_response) + else + let key = r.owner, r.token_id in + let bal = get_balance_amt (key, ledger) in + let response : balance_of_response = { request = r; balance = bal; } in + response + in + let responses = List.map to_balance p.requests in + Tezos.transaction responses 0mutez p.callback + + +let fa2_main (param, storage : fa2_entry_points * multi_ft_token_storage) + : (operation list) * multi_ft_token_storage = + match param with + | Transfer txs -> + (* + will validate that a sender is either `from_` parameter of each transfer + or a permitted operator for the owner `from_` address. + *) + let new_ledger = transfer (txs, default_operator_validator, storage) in + let new_storage = { storage with ledger = new_ledger; } + in ([] : operation list), new_storage + + | Royalty_transfer rts -> + royalty_transfer (rts, default_operator_validator, storage) + + | Balance_of p -> + let op = get_balance (p, storage.ledger, storage.token_metadata) in + [op], storage + + | Update_operators updates -> + let new_ops = fa2_update_operators (updates, storage.operators) in + let new_storage = { storage with operators = new_ops; } in + ([] : operation list), new_storage + +#endif