From 860dc9150642cb2e162aa27e69a3c984ca2fe2d8 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Thu, 11 Jun 2020 00:19:00 +0200 Subject: [PATCH 01/16] [Cameligo] fa2 implementation + course.md; separate FA2 chapter into 3 parts --- .../Chapters/Camel/ChapterFA20/caller.mligo | 53 ++++ .../Chapters/Camel/ChapterFA20/course.md | 255 ++++++++++++++++++ .../Chapters/Camel/ChapterFA20/exercise.mligo | 154 +++++++++++ .../ChapterFA20/tzip-12/fa2_errors.mligo | 44 +++ .../Camel/ChapterFA20/tzip-12/fa2_hook.mligo | 24 ++ .../ChapterFA20/tzip-12/fa2_interface.mligo | 219 +++++++++++++++ .../tzip-12/lib/fa2_behaviors.mligo | 87 ++++++ .../tzip-12/lib/fa2_convertors.mligo | 187 +++++++++++++ .../tzip-12/lib/fa2_hook_lib.mligo | 40 +++ .../tzip-12/lib/fa2_operator_lib.mligo | 64 +++++ .../Chapters/Camel/ChapterFA20Hook/course.md | 210 +++++++++++++++ .../Camel/ChapterFA20Permission/course.md | 164 +++++++++++ 12 files changed, 1501 insertions(+) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_errors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_hook.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_interface.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_behaviors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_convertors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_hook_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_operator_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo new file mode 100644 index 0000000..a4d6843 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo @@ -0,0 +1,53 @@ +#include "tzip/proposals/tzip-12/lib/fa2_hook_lib.mligo" +#include "tzip/proposals/tzip-12/lib/fa2_behaviors.mligo" + +type storage = { + rep : balance_of_response_michelson list +} + +type request_balance_of_param = { + at : address; + requests : balance_of_request list; +} + +type receive_balance_of_param = balance_of_response_michelson list + +let request_balance (req, s : (request_balance_of_param * storage)) : operation list * storage = + // Get the TZIP-12 contract instance 'from' the chain + let token_contract_balance_entrypoint_opt : balance_of_param contract option = Tezos.get_entrypoint_opt "%balance_of" req.at in + let token_contract_balance_entrypoint : balance_of_param contract = match (token_contract_balance_entrypoint_opt) with + | Some (ci) -> ci + | None -> (failwith("Entrypoint not found"): balance_of_param contract) + in + + // Callback (contract) for Balance_of will be the current contract's Receive_balance entrypoint + let balance_of_callback_contract_opt : receive_balance_of_param contract option = Tezos.get_entrypoint_opt "%receive_balance" Tezos.self_address in + let balance_of_callback_contract : receive_balance_of_param contract = match (balance_of_callback_contract_opt) with + | Some (ci) -> ci + | None -> (failwith("Entrypoint not found"): receive_balance_of_param contract) + in + //let balance_of_callback_contract : receive_balance_of_param contract = get_entrypoint("%receive_balance", Tezos.self_address) in + // Set up the parameter w/ data required for the Balance_of entrypoint + let balance_of_operation_param : balance_of_param = { + requests = req.requests; + callback = balance_of_callback_contract; + } in + // Forge an internal transaction to the TZIP-12 contract + // parametrised by the prieviously prepared `balance_of_operation_param` + // Note: We're sending 0mutez as part of this transaction + let balance_of_operation : operation = Tezos.transaction balance_of_operation_param 0mutez token_contract_balance_entrypoint in + ([ balance_of_operation ], s) + +let receive_balance (received, s: (receive_balance_of_param * storage)) : operation list * storage = + let new_store : storage = { s with rep = received } in + (([] : operation list), new_store) + +type entry_points = + | Request_balance of request_balance_of_param + | Receive_balance of receive_balance_of_param + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Request_balance request_balance_of_param -> request_balance (request_balance_of_param, s) + | Receive_balance receive_balance_of_param -> receive_balance (receive_balance_of_param, s) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md new file mode 100644 index 0000000..61a809e --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md @@ -0,0 +1,255 @@ +# Chapter 23 : Financial Asset 2.0 + +Captain, Let's create a ship token. + +## Definition + +There are multiple dimensions and considerations while implementing a particular token smart contract. Tokens might be fungible or non-fungible. A variety of +permission policies can be used to define how many tokens can be transferred, who can initiate a transfer, and who can receive tokens. A token contract can be +designed to support a single token type (e.g. ERC-20 or ERC-721) or multiple token types (e.g. ERC-1155) to optimize batch transfers and atomic swaps of the tokens. + +The FA2 standard proposes a *unified token contract interface* that accommodates all mentioned concerns. It aims to provide significant expressivity to contract developers to create new types of tokens while maintaining a common interface standard for wallet integrators and external developers. + +In the following chapter on Financial Asset 2.0 , we will focus on *TZIP-12* which stands for the 12th Tezos Improvement Proposal (same as EIP-721 for Ethereum). + +## Interface and library + +The FA2 interface formalize a standard way to design tokens and thus describes a list of entry points (that must be implemented) and data structures related to those entry points. A more detailed decription of the interface is broken down in following sections. + +In addition to the FA2 interface, the FA2 standard provides helper functions to manipulate data structures involved in FA2 interface. The FA2 library contains helper functions for : +* a generic behavior and transfer hook implementation (behavior based on *permissions_descriptor*), +* converting data structures, +* defining hooks between contracts when transfer is emitted, +* defining operators for managing allowance. + +## Entry points + +Token contract implementing the FA2 standard MUST have the following entry points. + +``` +type fa2_entry_points = + +| Transfer of transfer list +| Balance_of of balance_of_param +| Total_supply of total_supply_param +| Token_metadata of token_metadata_param +| Permissions_descriptor of permissions_descriptor contract +| Update_operators of update_operator list +| Is_operator of is_operator_param +``` + + +### Metadata + +FA2 token contracts MUST implement the *token_metadata* entry point which get the metadata for multiple token types. + +It accepts a list of *token_ids* and a callback contract, and sends back a list of *token_metadata* records. + +FA2 token amounts are represented by natural numbers (nat), and their granularity (the smallest amount of tokens which may be minted, burned, or +transferred) is always 1. + +The *decimals* property is the number of digits to use after the decimal point when displaying the token amounts. If 0, the asset is not divisible. Decimals are used for display purposes only and MUST NOT affect transfer operation. + + +#### Interface + +``` +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} + +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param = { + token_ids : token_id list; + callback : (token_metadata_michelson list) contract; +} +``` + + +### Balance of + +FA2 token contracts MUST implement the _Balance of_ entry point which get the balance of multiple account/token pairs (because FA2 supports mutiple token). +``` +| Balance_of of balance_of_param +``` + +It accepts a list of balance_of_requests and a callback and sends back to a callback contract a list of balance_of_response records. + +If one of the specified token_ids is not defined within the FA2 contract, the entry point MUST fail with the error mnemonic "TOKEN_UNDEFINED" (see section Error Handling). + +#### Interface + +The FA2 interface defines request/response parameters as follow : +``` +type token_id = nat + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} +``` + +### Totalsupply + +FA2 token contracts MUST implement the _Totalsupply_ entry point which get the total supply of tokens for multiple token types (because FA2 supports mutiple token). +``` +| Total_supply of total_supply_param +``` + +It accepts a list of *token_ids* and a callback, and sends back to the callback contract a list of *total_supply_response* records. + +If one of the specified token_ids is not defined within the FA2 contract, the entry point MUST fail with the error mnemonic "TOKEN_UNDEFINED" (see section Error Handling). + +#### Interface + +``` +type token_id = nat + +type total_supply_response = { + token_id : token_id; + total_supply : nat; +} + +type total_supply_response_michelson = total_supply_response michelson_pair_right_comb + +type total_supply_param = { + token_ids : token_id list; + callback : (total_supply_response_michelson list) contract; +} +``` + +### Transfer + +FA2 token contracts MUST implement the _Transfer_ entry point which transfer tokens between and MUST ensure following rules. +``` +| Transfer of transfer list +``` + +#### Rules + +FA2 token contracts MUST implement the transfer logic defined by the following rules : + + +1) Every transfer operation MUST be atomic. If the operation fails, all token transfers MUST be reverted, and token balances MUST remain unchanged. + +2) The amount of a token transfer MUST NOT exceed the existing token owner's balance. If the transfer amount for the particular token type and token owner +exceeds the existing balance, the whole transfer operation MUST fail with the error mnemonic "INSUFFICIENT_BALANCE" + +3) Core transfer behavior MAY be extended. If additional constraints on tokens transfer are required, FA2 token contract implementation MAY invoke additional +permission policies (transfer hook is the recommended design pattern to implement core behavior extension). (See Chapter FA2 - Hook) + +If the additional permission hook fails, the whole transfer operation MUST fail with a custom error mnemonic. + +4) Core transfer behavior MUST update token balances exactly as the operation parameters specify it. No changes to amount values or additional transfers are +allowed. + + + +#### Interface + +It transfer tokens from a *from_* account to possibly many destination accounts where each destination transfer describes the type of token, the amount of token, and receiver address. + +``` +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + + +``` + +### Error Handling + +This FA2 tandard defines the set of standard errors to make it easier to integrate FA2 contracts with wallets, DApps and other generic software, and enable +localization of user-visible error messages. + +Each error code is a short abbreviated string mnemonic. An FA2 contract client (like another contract or a wallet) could use on-the-chain or off-the-chain registry to map the error code mnemonic to a user-readable, localized message. + +A particular implementation of the FA2 contract MAY extend the standard set of errors with custom mnemonics for additional constraints. + +When error occurs, any FA2 contract entry point MUST fail with one of the following types: +* string value which represents an error code mnemonic. +* a Michelson pair, where the first element is a string representing error code mnemonic and the second element is a custom error data. + +#### Standard error mnemonics: + +Error mnemonic +Description + + + + +"TOKEN_UNDEFINED" +One of the specified token_ids is not defined within the FA2 contract + + +"INSUFFICIENT_BALANCE" +A token owner does not have sufficient balance to transfer tokens from owner's account + + +"TX_DENIED" +A transfer failed because of operator_transfer_policy == No_transfer + + + +"NOT_OWNER" +A transfer failed because operator_transfer_policy == Owner_transfer and it is initiated not by the token owner + + +"NOT_OPERATOR" +A transfer failed because operator_transfer_policy == Owner_or_operator_transfer and it is initiated neither by the token owner nor a permitted operator + + +"RECEIVER_HOOK_FAILED" +The receiver hook failed. This error MUST be raised by the hook implementation + + +"SENDER_HOOK_FAILED" +The sender failed. This error MUST be raised by the hook implementation + + +"RECEIVER_HOOK_UNDEFINED" +Receiver hook is required by the permission behavior, but is not implemented by a receiver contract + + +"SENDER_HOOK_UNDEFINED" +Sender hook is required by the permission behavior, but is not implemented by a sender contract + + + + +## Your mission + + +1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. + diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo new file mode 100644 index 0000000..fca09fd --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo @@ -0,0 +1,154 @@ +#include "tzip/proposals/tzip-12/fa2_interface.mligo" +#include "tzip/proposals/tzip-12/fa2_errors.mligo" + +type entity = { + balance : nat +} +type entity_key = address * token_id +type ledger = (entity_key, entity) map + +type tokens = { + total_supply : nat; + metadata : token_metadata; +} + +type operator_ = { + owner : address; + operator : address; +} + +type storage = { + paused : bool; + ledger : ledger; + tokens : (nat,tokens) map; + operators : operator_ set; + administrator : address; +} + +type return = (operation list * storage) + +type entry_points = + | Set_pause of bool + | Set_administrator of address + | MyBalance_of of balance_of_param + | MyTotal_supply of total_supply_param_michelson + | MyToken_metadata of token_metadata_param_michelson + | MyTransfer of transfer_michelson list + +let set_pause(param,s : bool * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with paused=param }) + else + (failwith("only admin can do it") : return) + +let set_administrator(param,s : address * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with administrator=param }) + else + (failwith("only admin can do it") : return) + +let balance_of (balance_of_param, s : balance_of_param * storage) : return = + let get_balance = fun ( i : balance_of_request) -> match Map.find_opt (i.owner,i.token_id) s.ledger with + | Some e -> { request = i ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response) + in + let balance_of_callback_param : balance_of_response list = List.map get_balance balance_of_param.requests in + let convert = fun ( r : balance_of_response) -> Layout.convert_to_right_comb(({request=Layout.convert_to_right_comb( r.request ); balance=r.balance} : balance_of_response_aux)) in + let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in + // sending back the processed map of balance requests/responses + let destination: (balance_of_response_michelson list) contract = balance_of_param.callback in + let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in + ([balance_of_response_operation], s) + +let total_supply(params, s: total_supply_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith("unknown token_id") : total_supply_response) + in + let responses : total_supply_response list = List.map get_total_supply token_ids in + let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in + let ret : total_supply_response_michelson list = List.map convert responses in + let destination: (total_supply_response_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let token_metadata(params, s: token_metadata_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : token_metadata_param = Layout.convert_from_right_comb(params: token_metadata_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> v.metadata + | None -> (failwith("unknown token_id") : token_metadata) + in + let responses : token_metadata list = List.map get_metadata token_ids in + let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in + let ret : token_metadata_michelson list = List.map convert responses in + let destination: (token_metadata_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let transfer(params, s: transfer_michelson list * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let apply_transfer = fun (l,i : ledger * transfer_michelson) -> + let t_aux : transfer_aux = Layout.convert_from_right_comb(i: transfer_michelson) in + let from_ : address = t_aux.from_ in + let result_ledger : ledger = if Tezos.sender = from_ or Tezos.sender = s.administrator then + if Set.mem {owner=from_;operator=Tezos.sender} s.operators then + let transfers : transfer_destination_michelson list = t_aux.txs in + let apply_transfer_destination = fun (acc,j : (ledger * transfer_destination_michelson)) -> + let transfer_destination : transfer_destination = Layout.convert_from_right_comb(j: transfer_destination_michelson) in + let tr_amount : nat = transfer_destination.amount in + let to_ : address = transfer_destination.to_ in + let tokenid : token_id = transfer_destination.token_id in + let temp_state_ledger : ledger = if tr_amount > 0n then + let enough_funds : bool = match Map.find_opt (from_,tokenid) acc with + | Some bal -> (bal.balance >= tr_amount) + | None -> false + in + if enough_funds then + let l_updated_from : ledger = match Map.find_opt (from_,tokenid) acc with + | Some bal -> Map.update (from_,tokenid) (Some {balance=abs(bal.balance - tr_amount)} ) acc + | None -> (failwith("should not arrive here") : ledger) + in + let l_updated_from_to : ledger = match Map.find_opt (to_,tokenid) l_updated_from with + | Some bal -> Map.update (to_,tokenid) (Some {balance=bal.balance + tr_amount}) l_updated_from + | None -> Map.add (to_,tokenid) {balance=tr_amount} l_updated_from + in + l_updated_from_to + else + (failwith(insufficient_balance) : ledger) + else + (failwith("transferring nothing !") : ledger) + in + temp_state_ledger + in + let result : ledger = List.fold apply_transfer_destination transfers l in + result + else + (failwith(not_operator) : ledger) + else + (failwith(not_owner) : ledger) + in + result_ledger + in + let new_ledger : ledger = List.fold apply_transfer params s.ledger in + (([] : operation list), new_ledger) + + +let main (p,s : entry_points * storage) : return = + match p with + | MyTotal_supply p -> total_supply (p,s) + | MyBalance_of p -> balance_of (p,s) + | MyToken_metadata p -> token_metadata (p,s) + | Set_pause p -> set_pause (p,s) + | Set_administrator p -> set_administrator (p,s) + | MyTransfer l -> transfer (l, s) \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_errors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_errors.mligo new file mode 100644 index 0000000..506f2e8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_errors.mligo @@ -0,0 +1,44 @@ +#if !FA2_ERRORS +#define FA2_ERRORS + +(** One of the specified `token_id`s is not defined within the FA2 contract *) +let token_undefined = "TOKEN_UNDEFINED" +(** +A token owner does not have sufficient balance to transfer tokens from +owner's account +*) +let insufficient_balance = "INSUFFICIENT_BALANCE" +(** A transfer failed because of `operator_transfer_policy == No_transfer` *) +let tx_denied = "TX_DENIED" +(** +A transfer failed because `operator_transfer_policy == Owner_transfer` and it is +initiated not by the token owner +*) +let not_owner = "NOT_OWNER" +(** +A transfer failed because `operator_transfer_policy == Owner_or_operator_transfer` +and it is initiated neither by the token owner nor a permitted operator + *) +let not_operator = "NOT_OPERATOR" +(** +Receiver hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let receiver_hook_failed = "RECEIVER_HOOK_FAILED" +(** +Sender hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let sender_hook_failed = "SENDER_HOOK_FAILED" +(** +Receiver hook is required by the permission behavior, but is not implemented by +a receiver contract + *) +let receiver_hook_undefined = "RECEIVER_HOOK_UNDEFINED" +(** +Sender hook is required by the permission behavior, but is not implemented by +a sender contract + *) +let sender_hook_undefined = "SENDER_HOOK_UNDEFINED" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_hook.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_hook.mligo new file mode 100644 index 0000000..6137386 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_hook.mligo @@ -0,0 +1,24 @@ + +#if !FA2_HOOK +#define FA2_HOOK + +#include "fa2_interface.mligo" + + +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb + +type fa2_with_hook_entry_points = + | Fa2 of fa2_entry_points + | Set_transfer_hook of set_hook_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_interface.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_interface.mligo new file mode 100644 index 0000000..c6043a0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_interface.mligo @@ -0,0 +1,219 @@ +#if ! FA2_INTERFACE +#define FA2_INTERFACE + +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + +type transfer_michelson = transfer_aux michelson_pair_right_comb + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_request_michelson = balance_of_request michelson_pair_right_comb + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_response_aux = { + request : balance_of_request_michelson; + balance : nat; +} + +type balance_of_response_michelson = balance_of_response_aux michelson_pair_right_comb + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_aux = { + requests : balance_of_request_michelson list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_michelson = balance_of_param_aux michelson_pair_right_comb + +type total_supply_response = { + token_id : token_id; + total_supply : nat; +} + +type total_supply_response_michelson = total_supply_response michelson_pair_right_comb + +type total_supply_param = { + token_ids : token_id list; + callback : (total_supply_response_michelson list) contract; +} + +type total_supply_param_michelson = total_supply_param michelson_pair_right_comb + +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} + +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param = { + token_ids : token_id list; + callback : (token_metadata_michelson list) contract; +} + +type token_metadata_param_michelson = token_metadata_param michelson_pair_right_comb + +type operator_param = { + owner : address; + operator : address; +} + +type operator_param_michelson = operator_param michelson_pair_right_comb + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type update_operator_aux = + | Add_operator of operator_param_michelson + | Remove_operator of operator_param_michelson + +type update_operator_michelson = update_operator_aux michelson_or_right_comb + +type is_operator_response = { + operator : operator_param; + is_operator : bool; +} + +type is_operator_response_aux = { + operator : operator_param_michelson; + is_operator : bool; +} + +type is_operator_response_michelson = is_operator_response_aux michelson_pair_right_comb + +type is_operator_param = { + operator : operator_param; + callback : (is_operator_response_michelson) contract; +} + +type is_operator_param_aux = { + operator : operator_param_michelson; + callback : (is_operator_response_michelson) contract; +} + +type is_operator_param_michelson = is_operator_param_aux michelson_pair_right_comb + +(* permission policy definition *) + +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer + +type operator_transfer_policy_michelson = operator_transfer_policy michelson_or_right_comb + +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook + +type owner_hook_policy_michelson = owner_hook_policy michelson_or_right_comb + +type custom_permission_policy = { + tag : string; + config_api: address option; +} + +type custom_permission_policy_michelson = custom_permission_policy michelson_pair_right_comb + +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} + +type permissions_descriptor_aux = { + operator : operator_transfer_policy_michelson; + receiver : owner_hook_policy_michelson; + sender : owner_hook_policy_michelson; + custom : custom_permission_policy_michelson option; +} + +type permissions_descriptor_michelson = permissions_descriptor_aux michelson_pair_right_comb + +type fa2_entry_points = + | Transfer of transfer_michelson list + | Balance_of of balance_of_param_michelson + | Total_supply of total_supply_param_michelson + | Token_metadata of token_metadata_param_michelson + | Permissions_descriptor of permissions_descriptor_michelson contract + | Update_operators of update_operator_michelson list + | Is_operator of is_operator_param_michelson + + +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param = { + fa2 : address; + batch : transfer_descriptor list; + operator : address; +} + +type transfer_descriptor_param_aux = { + fa2 : address; + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb + +type fa2_token_receiver = + | Tokens_received of transfer_descriptor_param_michelson + +type fa2_token_sender = + | Tokens_sent of transfer_descriptor_param_michelson + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_behaviors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_behaviors.mligo new file mode 100644 index 0000000..dd933ac --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_behaviors.mligo @@ -0,0 +1,87 @@ +#if !FA2_BEHAVIORS +#define FA2_BEHAVIORS + +(* #include "fa2_hook_lib.mligo" *) +#include "../fa2_interface.mligo" +#include "../fa2_errors.mligo" + + +(** generic transfer hook implementation. Behavior is driven by `permissions_descriptor` *) + +type get_owners = transfer_descriptor -> (address option) list +type to_hook = address -> ((transfer_descriptor_param_michelson contract) option * string) +type transfer_hook_params = { + ligo_param : transfer_descriptor_param; + michelson_param : transfer_descriptor_param_michelson; +} + +let get_owners_from_batch (batch, get_owners : (transfer_descriptor list) * get_owners) : address set = + List.fold + (fun (acc, tx : (address set) * transfer_descriptor) -> + let owners = get_owners tx in + List.fold + (fun (acc, o: (address set) * (address option)) -> + match o with + | None -> acc + | Some a -> Set.add a acc + ) + owners + acc + ) + batch + (Set.empty : address set) + +let validate_owner_hook (p, get_owners, to_hook, is_required : + transfer_hook_params * get_owners * to_hook * bool) + : operation list = + let owners = get_owners_from_batch (p.ligo_param.batch, get_owners) in + Set.fold + (fun (ops, owner : (operation list) * address) -> + let hook, error = to_hook owner in + match hook with + | Some h -> + let op = Operation.transaction p.michelson_param 0mutez h in + op :: ops + | None -> + if is_required + then (failwith error : operation list) + else ops) + owners ([] : operation list) + +let validate_owner(p, policy, get_owners, to_hook : + transfer_hook_params * owner_hook_policy * get_owners * to_hook) + : operation list = + match policy with + | Owner_no_hook -> ([] : operation list) + | Optional_owner_hook -> validate_owner_hook (p, get_owners, to_hook, false) + | Required_owner_hook -> validate_owner_hook (p, get_owners, to_hook, true) + +let to_receiver_hook : to_hook = fun (a : address) -> + let c : (transfer_descriptor_param_michelson contract) option = + Operation.get_entrypoint_opt "%tokens_received" a in + c, receiver_hook_undefined + +let validate_receivers (p, policy : transfer_hook_params * owner_hook_policy) + : operation list = + let get_receivers : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_ )tx.txs in + validate_owner (p, policy, get_receivers, to_receiver_hook) + +let to_sender_hook : to_hook = fun (a : address) -> + let c : (transfer_descriptor_param_michelson contract) option = + Operation.get_entrypoint_opt "%tokens_sent" a in + c, sender_hook_undefined + +let validate_senders (p, policy : transfer_hook_params * owner_hook_policy) + : operation list = + let get_sender : get_owners = fun (tx : transfer_descriptor) -> [tx.from_] in + validate_owner (p, policy, get_sender, to_sender_hook) + +let standard_transfer_hook (p, descriptor : transfer_hook_params * permissions_descriptor) + : operation list = + let sender_ops = validate_senders (p, descriptor.sender) in + let receiver_ops = validate_receivers (p, descriptor.receiver) in + (* merge two lists *) + List.fold (fun (l, o : (operation list) * operation) -> o :: l) receiver_ops sender_ops + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_convertors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_convertors.mligo new file mode 100644 index 0000000..9cc7c6c --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_convertors.mligo @@ -0,0 +1,187 @@ +#if !FA2_CONVERTORS +#define FA2_CONVERTORS + +#include "../fa2_interface.mligo" + +let permissions_descriptor_to_michelson (d : permissions_descriptor) + : permissions_descriptor_michelson = + let aux : permissions_descriptor_aux = { + operator = Layout.convert_to_right_comb d.operator; + receiver = Layout.convert_to_right_comb d.receiver; + sender = Layout.convert_to_right_comb d.sender; + custom = match d.custom with + | None -> (None : custom_permission_policy_michelson option) + | Some c -> Some (Layout.convert_to_right_comb c) + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_to_michelson (p : transfer_descriptor) : transfer_descriptor_michelson = + let aux : transfer_descriptor_aux = { + from_ = p.from_; + txs = List.map + (fun (tx : transfer_destination_descriptor) -> + Layout.convert_to_right_comb tx + ) + p.txs; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_param_to_michelson (p : transfer_descriptor_param) + : transfer_descriptor_param_michelson = + let aux : transfer_descriptor_param_aux = { + fa2 = p.fa2; + operator = p.operator; + batch = List.map + (fun (td: transfer_descriptor) -> transfer_descriptor_to_michelson td) + p.batch; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_from_michelson (p : transfer_descriptor_michelson) : transfer_descriptor = + let aux : transfer_descriptor_aux = Layout.convert_from_right_comb p in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_descriptor_michelson) -> + let tx : transfer_destination_descriptor = + Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfer_descriptor_param_from_michelson (p : transfer_descriptor_param_michelson) + : transfer_descriptor_param = + let aux : transfer_descriptor_param_aux = Layout.convert_from_right_comb p in + let b : transfer_descriptor list = List.map + (fun (tdm : transfer_descriptor_michelson) -> + transfer_descriptor_from_michelson tdm + ) + aux.batch + in + { + fa2 = aux.fa2; + operator = aux.operator; + batch = b; + } + +let transfer_from_michelson (txm : transfer_michelson) : transfer = + let aux : transfer_aux = Layout.convert_from_right_comb txm in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_michelson) -> + let tx : transfer_destination = Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfers_from_michelson (txsm : transfer_michelson list) : transfer list = + List.map + (fun (txm: transfer_michelson) -> + let tx : transfer = transfer_from_michelson txm in + tx + ) txsm + +let operator_param_from_michelson (p : operator_param_michelson) : operator_param = + let op : operator_param = Layout.convert_from_right_comb p in + op + +let operator_param_to_michelson (p : operator_param) : operator_param_michelson = + Layout.convert_to_right_comb p + +let operator_update_from_michelson (uom : update_operator_michelson) : update_operator = + let aux : update_operator_aux = Layout.convert_from_right_comb uom in + match aux with + | Add_operator opm -> Add_operator_p (operator_param_from_michelson opm) + | Remove_operator opm -> Remove_operator_p (operator_param_from_michelson opm) + +let operator_update_to_michelson (uo : update_operator) : update_operator_michelson = + let aux = match uo with + | Add_operator_p op -> Add_operator (operator_param_to_michelson op) + | Remove_operator_p op -> Remove_operator (operator_param_to_michelson op) + in + Layout.convert_to_right_comb aux + +(* check this *) +let operator_updates_from_michelson (updates_michelson : update_operator_michelson list) + : update_operator list = + List.map operator_update_from_michelson updates_michelson + +let is_operator_param_from_michelson (p : is_operator_param_michelson) : is_operator_param = + let aux : is_operator_param_aux = Layout.convert_from_right_comb p in + { + operator = operator_param_from_michelson aux.operator; + callback = aux.callback; + } + +let is_operator_param_to_michelson (p : is_operator_param) : is_operator_param_michelson = + let aux : is_operator_param_aux = + { + operator = operator_param_to_michelson p.operator; + callback = p.callback; + } in + Layout.convert_to_right_comb aux + +let is_operator_response_to_michelson (r : is_operator_response) : is_operator_response_michelson = + let aux : is_operator_response_aux = { + operator = operator_param_to_michelson r.operator; + is_operator = r.is_operator; + } in + Layout.convert_to_right_comb aux + +let balance_of_param_from_michelson (p : balance_of_param_michelson) : balance_of_param = + let aux : balance_of_param_aux = Layout.convert_from_right_comb p in + let requests = List.map + (fun (rm : balance_of_request_michelson) -> + let r : balance_of_request = Layout.convert_from_right_comb rm in + r + ) + aux.requests + in + { + requests = requests; + callback = aux.callback; + } + +let balance_of_param_to_michelson (p : balance_of_param) : balance_of_param_michelson = + let aux : balance_of_param_aux = { + requests = List.map + (fun (r : balance_of_request) -> Layout.convert_to_right_comb r) + p.requests; + callback = p.callback; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_to_michelson (r : balance_of_response) : balance_of_response_michelson = + let aux : balance_of_response_aux = { + request = Layout.convert_to_right_comb r.request; + balance = r.balance; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_from_michelson (rm : balance_of_response_michelson) : balance_of_response = + let aux : balance_of_response_aux = Layout.convert_from_right_comb rm in + let request : balance_of_request = Layout.convert_from_right_comb aux.request in + { + request = request; + balance = aux.balance; + } + +let total_supply_responses_to_michelson (rs : total_supply_response list) + : total_supply_response_michelson list = + List.map + (fun (r : total_supply_response) -> + let rm : total_supply_response_michelson = Layout.convert_to_right_comb r in + rm + ) rs + +let token_metas_to_michelson (ms : token_metadata list) : token_metadata_michelson list = + List.map + ( fun (m : token_metadata) -> + let mm : token_metadata_michelson = Layout.convert_to_right_comb m in + mm + ) ms + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_hook_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_hook_lib.mligo new file mode 100644 index 0000000..de1e3c1 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_hook_lib.mligo @@ -0,0 +1,40 @@ +#if !FA2_HOOK_LIB +#define FA2_HOOK_LIB + +#include "../fa2_hook.mligo" +#include "fa2_convertors.mligo" + +let get_hook_entrypoint (hook_contract : address) (u : unit) + : transfer_descriptor_param_michelson contract = + let hook_entry : transfer_descriptor_param_michelson contract = + Operation.get_entrypoint "%tokens_transferred_hook" hook_contract in + hook_entry + + +let create_register_hook_op + (fa2, descriptor : (fa2_with_hook_entry_points contract) * permissions_descriptor) : operation = + let hook_fn = get_hook_entrypoint Current.self_address in + let p : set_hook_param_aux = { + hook = hook_fn; + permissions_descriptor = permissions_descriptor_to_michelson descriptor; + } in + let pm = Layout.convert_to_right_comb p in + Operation.transaction (Set_transfer_hook pm) 0mutez fa2 + + +type fa2_registry = address set + +let register_with_fa2 (fa2, descriptor, registry : + (fa2_with_hook_entry_points contract) * permissions_descriptor * fa2_registry) + : operation * fa2_registry = + let op = create_register_hook_op (fa2, descriptor) in + let fa2_address = Current.address fa2 in + let new_registry = Set.add fa2_address registry in + op, new_registry + +let validate_hook_call (fa2, registry: address * fa2_registry) : unit = + if Set.mem fa2 registry + then unit + else failwith "UNKNOWN_FA2_CALL" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_operator_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_operator_lib.mligo new file mode 100644 index 0000000..4872180 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_operator_lib.mligo @@ -0,0 +1,64 @@ +(** Reference implementation of the FA2 operator storage and config API functions *) +#if !FA2_OPERATOR_LIB +#define FA2_OPERATOR_LIB + +#include "fa2_convertors.mligo" +#include "../fa2_errors.mligo" + +(* (owner, operator) -> unit *) +type operator_storage = ((address * address), unit) big_map + + +let update_operators (update, storage : update_operator * operator_storage) + : operator_storage = + match update with + | Add_operator_p op -> + Big_map.update (op.owner, op.operator) (Some unit) storage + | Remove_operator_p op -> + Big_map.remove (op.owner, op.operator) storage + +let validate_update_operators_by_owner (update, updater : update_operator * address) + : unit = + let op = match update with + | Add_operator_p op -> op + | Remove_operator_p op -> op + in + if op.owner = updater then unit else failwith not_owner + + +let is_operator (param, storage : is_operator_param * operator_storage) : operation = + let op_key = (param.operator.owner, param.operator.operator) in + let is_op = Big_map.mem op_key storage in + let r : is_operator_response = { + operator = param.operator; + is_operator = is_op; + } in + let rm = is_operator_response_to_michelson r in + Operation.transaction rm 0mutez param.callback + +let make_operator_validator (tx_policy : operator_transfer_policy) + : (address * operator_storage)-> unit = + let can_owner_tx, can_operator_tx = match tx_policy with + | No_transfer -> (failwith tx_denied : bool * bool) + | Owner_transfer -> true, false + | Owner_or_operator_transfer -> true, true + in + let operator : address = Tezos.sender in + (fun (owner, ops_storage : address * operator_storage) -> + if can_owner_tx && owner = operator + then unit + else + if not can_operator_tx + then failwith not_owner + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith not_operator + ) + +(** validate operators for all transfers in the batch at once*) +let validate_operator (tx_policy, txs, ops_storage + : operator_transfer_policy * (transfer list) * operator_storage) : unit = + let validator = make_operator_validator tx_policy in + List.iter (fun (tx : transfer) -> validator (tx.from_, ops_storage)) txs + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md new file mode 100644 index 0000000..98df4ee --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md @@ -0,0 +1,210 @@ +# Chapter 25 : Financial Asset 2.0 - Tranfer Hook + +Captain, all space pirate should have a hook like in old times. + +## ... in the previous episode + +The FA2 standard proposes a *unified token contract interface* that accommodates all mentioned concerns. It aims to provide significant expressivity to contract developers to create new types of tokens while maintaining a common interface standard for wallet integrators and external developers. + +The FA2 interface formalize a standard way to design tokens and thus describes a list of entrypoints (that must be implemented) and data structures related to those entrypoints. + +In this chapter we will focus on _transfer hook_ + +### Transfer Hook + +The FA2 standard proposes an approach in which a pluggable separate contract (permission transfer hook) is implemented and registered with the core FA2. Every time FA2 performs a transfer, it invokes a "hook" contract that validates a transaction and either approves it by finishing execution successfully or rejects it by failing. + + +#### definition + +_Transfer hook_ is one recommended design pattern to implement FA2 that enables separation of the core token transfer logic and a permission policy. + +Instead of implementing FA2 as a monolithic contract, a permission policy can be implemented as a separate contract. Permission policy contract provides an entry point invoked by the core FA2 contract to accept or reject a particular transfer operation (such an entry point is called *transfer hook*). + +Although this approach introduces gas consumption overhead (compared to an all-in-one contract) by requiring an extra inter-contract call, it also offers some other advantages: +1) FA2 core implementation can be verified once, and certain properties (not related to permission policy) remain unchanged. +2) modification of the permission policy of an existing contract can be done by replacing a transfer hook only. No storage migration of the FA2 ledger is required. +3) Transfer hooks could be used for purposes beyond permissioning, such as implementing _custom logic_ for a particular token application + +The transfer hook makes it possible to model different transfer permission policies like whitelists, operator lists, etc. + + +#### Hook interface + +The FA2 interface formalize a standard way to handle hooks. + +``` +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb + +type fa2_with_hook_entry_points = + | Fa2 of fa2_entry_points + | Set_transfer_hook of set_hook_param_michelson +``` + +In addition to the hook standard, the FA2 standard provides helper functions to manipulate data structures involved in FA2 interface. These helper function are packed in a FA2 library. (see section "FA2 standard hook library") + +#### FA2 standard hook library + +Some helpers functions has been gatthered in a hook library which help defining hooks when implementing a FA2 contract. This library contains following functions and type alias : + +The type _fa2_registry_ is a set of address + +the function *get_hook_entrypoint* retrieves the contract interface of entrypoint "%tokens_transferred_hook" for a given contract address + +the function *register_with_fa2* +* takes the address of a FA2 contract (having hooks) and register it in the registry (set of address). +* calls the *Set_transfer_hook* entrypoint of a FA2 contract + +the function *create_register_hook_op* sends a transaction to a FA2 contract (having hook entrypoints). The transaction intends to invoke the entrypoint *Set_transfer_hook*. This entrypoint *Set_transfer_hook* requires as parameters : +* the contract interface of entrypoint "%tokens_transferred_hook" +* a _permission descriptor_ + +the function *validate_hook_call* ensures an address in registered in the registry (set of address). + +#### Hook Rules + +FA2 implementation with the transfer hook pattern recquires following rules: + +1) An FA2 token contract has a single entry point to set the hook. If a transfer hook is not set, the FA2 token contract transfer operation MUST fail. + +2) Transfer hook is to be set by the token contract administrator before any transfers can happen. + +3) The concrete token contract implementation MAY impose additional restrictions on +who may set the hook. If the set hook operation is not permitted, it MUST fail +without changing existing hook configuration. + +4) For each transfer operation, a token contract MUST invoke a transfer hook and +return a corresponding operation as part of the transfer entry point result. +(For more details see set_transfer_hook ) + +5) *operator* parameter for the hook invocation MUST be set to *SENDER*. + +6) *from_* parameter for each hook_transfer batch entry MUST be set to *Some(transfer.from_)*. + +7) *to_* parameter for each hook_transfer batch entry MUST be set to *Some(transfer.to_)*. + +8) A transfer hook MUST be invoked, and operation returned by the hook invocation +MUST be returned by transfer entry point among other operations it might create. +*SENDER* MUST be passed as an operator parameter to any hook invocation. If an +invoked hook fails, the whole transfer transaction MUST fail. + +9) FA2 does NOT specify an interface for mint and burn operations; however, if an +FA2 token contract implements mint and burn operations, these operations MUST +invoke a transfer hook as well. + +#### Implementation of a custom hook + +Let's see an example of FA2 implementation. The following smart contract implements a token where transfer receiver must be in a whitelist. This whitelisting is done via a tranfer hook. +It uses a combination of a receiver white list and *fa2_token_receiver* interface. +Transfer is permitted if a receiver address is in the receiver white list OR implements *fa2_token_receiver* interface. +If a receiver address implements *fa2_token_receiver* interface, its *tokens_received* entry point must be called. + +``` +#include "../lib/fa2_hook_lib.mligo" +#include "../lib/fa2_behaviors.mligo" + + +type storage = { + fa2_registry : fa2_registry; + receiver_whitelist : address set; +} + +let custom_validate_receivers (p, wl : transfer_descriptor_param * (address set)) + : operation list = + let get_receiver : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in + let receivers = get_owners_from_batch (p.batch, get_receiver) in + + Set.fold + (fun (ops, r : (operation list) * address) -> + let hook, err = to_sender_hook r in + match hook with + | Some h -> + let pm = transfer_descriptor_param_to_michelson p in + let op = Operation.transaction pm 0mutez h in + op :: ops + | None -> + if Set.mem r wl + then ops + else (failwith err : operation list) + ) + receivers ([] : operation list) + +let custom_transfer_hook (p, s : transfer_descriptor_param * storage) : operation list = + custom_validate_receivers (p, s.receiver_whitelist) + + +let get_policy_descriptor (u : unit) : permissions_descriptor = + { + operator = Owner_or_operator_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook ; (* overridden by the custom policy *) + custom = Some { + tag = "receiver_hook_and_whitelist"; + config_api = (Some Current.self_address); + }; + } + +type config_whitelist = + | Add_receiver_to_whitelist of address set + | Remove_receiver_from_whitelist of address set + +let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) + : address set = + match cfg with + | Add_receiver_to_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.add a l) + rs wl + | Remove_receiver_from_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.remove a l) + rs wl + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_receiver_whitelist of config_whitelist + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook p -> + // verify s.fa2_registry contains p.fa2 address otherwise throw exception + let u = validate_hook_call (p.fa2, s.fa2_registry) in + let ops = custom_transfer_hook (p, s) in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = get_policy_descriptor unit in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_receiver_whitelist cfg -> + let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in + let new_s = { s with receiver_whitelist = new_wl; } in + ([] : operation list), new_s + +``` + + + +## Your mission + + + +1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. + + + diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md new file mode 100644 index 0000000..c97051d --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md @@ -0,0 +1,164 @@ +# Chapter 24 : Financial Asset 2.0 - Operators and Permissions + +Captain, why are you trying to change the part yourself? Just write a function on the terminal and send it to a droid. + +## Definition + +The FA2 standard proposes a *unified token contract interface* that accommodates all mentioned concerns. It aims to provide significant expressivity to contract developers to create new types of tokens while maintaining a common interface standard for wallet integrators and external developers. + +In this chapter we will focus on _Operators_ and _Permissions_. + + +### Operators + +#### Definition + +_Operator_ is a Tezos address that initiates token transfer operation on behalf of the owner. +_Owner_ is a Tezos address which can hold tokens. +An operator, other than the owner, MUST be approved to manage particular token types held by the owner to make a transfer from the owner account. +Operator relation is not transitive. If C is an operator of B , and if B is an operator of A, C cannot transfer tokens that are owned by A, on behalf of B. + +an _operator_ is defined as a relationship between two address (owner address and operator address) and can be understood as an operator address who can operate tokens held by a owner. + +#### FA2 interface operator + +FA2 interface specifies two entry points to update and inspect operators. Once permitted for the specific token owner, an operator can transfer any tokens belonging to the owner. + +``` +| Update_operators of update_operator list +| Is_operator of is_operator_param +``` + +where parameter type *update_operator* and *is_operator_param* are : +``` +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type is_operator_param = { + operator : operator_param; + callback : (is_operator_response_michelson) contract; +} +``` + +Notice the *update_operator* can only Add or Remove an _operator_ (an allowance between an operator address and a token owner address). + +Notice the parameter _is_operator_param_ given to *Is_operator* entry point contains a *callback* property used to send back a response to the calling contract. + +#### FA2 standard operator library + +Some helpers functions has been implemented in the FA2 library which help manipulating _operator_. This library contains following functions and type alias : + + +an _operator_ is a relationship between two address (owner address and operator address) + +function *is_operator* returns to a contract caller whether an operator address is associated to an owner address + +function *update_operators* allows to Add or Remove an operator in the list of operators. + +function *validate_update_operators_by_owner*, it ensures the given adress is owner of an _operator_ + +the function *validate_operator* validates operators for all transfers in the batch at once, depending on given operator_transfer_policy + + + +### FA2 Permission Policies and Configuration + +Most token standards specify logic that validates a transfer transaction and can either approve or reject a transfer. +Such logic (called _Permission Policy_) could validate who initiates a transfer, the transfer amount, and who can receive tokens. + +This FA2 standard defines a framework to compose and configure such permission policies from the standard behaviors and configuration APIs. + +FA2 defines : +* the default core transfer behavior, that MUST always be implemented +* a set of predefined permission policies that are optional + + +#### permissions_descriptor + +FA2 specifies an interface permissions_descriptor allowing external contracts to discover an FA2 contract's permission policy and to configure it. *permissions_descriptor* serves as a modular approach to define consistent and non-self-contradictory policies. + +The *permission descriptor* indicates which standard permission policies are implemented by the FA2 contract and can be used by off-chain and on-chain tools to discover the properties of the particular FA2 contract implementation. + +The FA2 standard defines a special metadata entry point *permission descriptor* containing standard permission policies. +``` +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} +``` + + +#### operator_transfer_policy + +operator_transfer_policy - defines who can transfer tokens. Tokens can be +transferred by the token owner or an operator (some address that is authorized to +transfer tokens on behalf of the token owner). A special case is when neither owner +nor operator can transfer tokens (can be used for non-transferable tokens). The +FA2 standard defines two entry points to manage and inspect operators associated +with the token owner address (*update_operators*, +*is_operator*). Once an operator is added, it can manage all of +its associated owner's tokens. + +``` +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer +``` + +#### owner_hook_policy + +owner_hook_policy - defines if sender/receiver hooks should be called or +not. Each token owner contract MAY implement either an *fa2_token_sender* or +*fa2_token_receiver* hook interface. Those hooks MAY be called when a transfer sends +tokens from the owner account or the owner receives tokens. The hook can either +accept a transfer transaction or reject it by failing. + +``` +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook +``` + +#### custom_permission_policy + +It is possible to extend permission policy with a custom behavior, which does +not overlap with already existing standard policies. This standard does not specify +exact types for custom config entry points. FA2 token contract clients that support +custom config entry points must know their types a priori and/or use a tag hint +of custom_permission_policy. + +``` +type custom_permission_policy = { + tag : string; + config_api: address option; +} +``` + + +#### Permission Policy Formulae + +Each concrete implementation of the permission policy can be described by a formula which combines permission behaviors in the following form: +``` +Operator(?) * Receiver(?) * Sender(?) +``` + +This formula describes the policy which allows only token owners to transfer their own +tokens : +``` +Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) +``` + + + + + +## Your mission + + +1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. + From 0c64aae70b48fae083c0c9dbac7358784f97a838 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Thu, 11 Jun 2020 11:03:27 +0200 Subject: [PATCH 02/16] [cameligo] FA2 part1 exercise --- .../Chapters/Camel/ChapterFA20/course.md | 59 +++---- .../Chapters/Camel/ChapterFA20/exercise.mligo | 9 +- .../pages/Chapters/Camel/ChapterFA20/index.ts | 11 ++ .../Chapters/Camel/ChapterFA20/solution.mligo | 155 ++++++++++++++++++ 4 files changed, 196 insertions(+), 38 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md index 61a809e..fa34671 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md @@ -12,6 +12,16 @@ The FA2 standard proposes a *unified token contract interface* that accommodates In the following chapter on Financial Asset 2.0 , we will focus on *TZIP-12* which stands for the 12th Tezos Improvement Proposal (same as EIP-721 for Ethereum). +## Architecture + +FA2 proposes to leave it to implementers to handle common considerations such as defining the contract’s token type(s) (e.g. non-fungible vs. fungible vs. semi-fungible), administration and whitelisting, contract upgradability, and supply operations (e.g. mint/burn). + +FA2 also leaves to implementers to decide on architecture pattern for handling permissioning. Permission can be implemented +* in the the same contract as the core transfer behavior (i.e. a “monolith”), +* in a transfer hook to another contract, +* in a separate wrapper contract. + + ## Interface and library The FA2 interface formalize a standard way to design tokens and thus describes a list of entry points (that must be implemented) and data structures related to those entry points. A more detailed decription of the interface is broken down in following sections. @@ -184,8 +194,6 @@ type transfer_aux = { from_ : address; txs : transfer_destination_michelson list; } - - ``` ### Error Handling @@ -203,53 +211,38 @@ When error occurs, any FA2 contract entry point MUST fail with one of the follow #### Standard error mnemonics: -Error mnemonic -Description - - - +Error mnemonic - Description -"TOKEN_UNDEFINED" -One of the specified token_ids is not defined within the FA2 contract +"TOKEN_UNDEFINED" - One of the specified token_ids is not defined within the FA2 contract +"INSUFFICIENT_BALANCE" - A token owner does not have sufficient balance to transfer tokens from owner's account -"INSUFFICIENT_BALANCE" -A token owner does not have sufficient balance to transfer tokens from owner's account +"TX_DENIED" - A transfer failed because of operator_transfer_policy == No_transfer +"NOT_OWNER" - A transfer failed because operator_transfer_policy == Owner_transfer and it is initiated not by the token owner -"TX_DENIED" -A transfer failed because of operator_transfer_policy == No_transfer +"NOT_OPERATOR" - A transfer failed because operator_transfer_policy == Owner_or_operator_transfer and it is initiated neither by the token owner nor a permitted operator +"RECEIVER_HOOK_FAILED" - The receiver hook failed. This error MUST be raised by the hook implementation +"SENDER_HOOK_FAILED" - The sender failed. This error MUST be raised by the hook implementation -"NOT_OWNER" -A transfer failed because operator_transfer_policy == Owner_transfer and it is initiated not by the token owner +"RECEIVER_HOOK_UNDEFINED" -Receiver hook is required by the permission behavior, but is not implemented by a receiver contract +"SENDER_HOOK_UNDEFINED" - Sender hook is required by the permission behavior, but is not implemented by a sender contract -"NOT_OPERATOR" -A transfer failed because operator_transfer_policy == Owner_or_operator_transfer and it is initiated neither by the token owner nor a permitted operator -"RECEIVER_HOOK_FAILED" -The receiver hook failed. This error MUST be raised by the hook implementation - - -"SENDER_HOOK_FAILED" -The sender failed. This error MUST be raised by the hook implementation - - -"RECEIVER_HOOK_UNDEFINED" -Receiver hook is required by the permission behavior, but is not implemented by a receiver contract - - -"SENDER_HOOK_UNDEFINED" -Sender hook is required by the permission behavior, but is not implemented by a sender contract +## Your mission + We want you to complete the existing implementation. The Total_supply entry point is not yet implemented , please finish the job ! +1 - Modify the *get_total_supply* function in order to retrieve the total_supply information related to the given *token_id* list. -## Your mission +2 -You must find each token in the *tokens* map. +3 -If a given token_id is found then the function *get_total_supply* must return a *total_supply_response* record for each *token_id*. As seen in the interface the *total_supply_response* record contains *token_id* and *total_supply* fields. (use v as temporary variable for the match with instruction) -1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. +4 -If a given token_id is not found then the function *get_total_supply* must throw an exception with the predefined error messsage *token_undefined*. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo index fca09fd..41dacbe 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo @@ -65,10 +65,9 @@ let total_supply(params, s: total_supply_param_michelson * storage) : return = (failwith("contract in pause") : return) else let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in - let token_ids : token_id list = p.token_ids in - let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with - | Some v -> { token_id = i ; total_supply =v.total_supply } - | None -> (failwith("unknown token_id") : total_supply_response) + let token_ids : token_id list = p.token_ids in + // Modify the code below + let get_total_supply = fun ( i : token_id) -> in let responses : total_supply_response list = List.map get_total_supply token_ids in let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in @@ -85,7 +84,7 @@ let token_metadata(params, s: token_metadata_param_michelson * storage) : return let token_ids : token_id list = p.token_ids in let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with | Some v -> v.metadata - | None -> (failwith("unknown token_id") : token_metadata) + | None -> (failwith(token_undefined) : token_metadata) in let responses : token_metadata list = List.map get_metadata token_ids in let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts new file mode 100644 index 0000000..5afd766 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts @@ -0,0 +1,11 @@ +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import course from "!raw-loader!./course.md"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import exercise from "!raw-loader!./exercise.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import solution from "!raw-loader!./solution.mligo"; + +export const data = { course, exercise, solution }; diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo new file mode 100644 index 0000000..b49aa9a --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo @@ -0,0 +1,155 @@ +#include "tzip/proposals/tzip-12/fa2_interface.mligo" +#include "tzip/proposals/tzip-12/fa2_errors.mligo" + +type entity = { + balance : nat +} +type entity_key = address * token_id +type ledger = (entity_key, entity) map + +type tokens = { + total_supply : nat; + metadata : token_metadata; +} + +type operator_ = { + owner : address; + operator : address; +} + +type storage = { + paused : bool; + ledger : ledger; + tokens : (nat,tokens) map; + operators : operator_ set; + administrator : address; +} + +type return = (operation list * storage) + +type entry_points = + | Set_pause of bool + | Set_administrator of address + | MyBalance_of of balance_of_param + | MyTotal_supply of total_supply_param_michelson + | MyToken_metadata of token_metadata_param_michelson + | MyTransfer of transfer_michelson list + +let set_pause(param,s : bool * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with paused=param }) + else + (failwith("only admin can do it") : return) + +let set_administrator(param,s : address * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with administrator=param }) + else + (failwith("only admin can do it") : return) + +let balance_of (balance_of_param, s : balance_of_param * storage) : return = + let get_balance = fun ( i : balance_of_request) -> match Map.find_opt (i.owner,i.token_id) s.ledger with + | Some e -> { request = i ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response) + in + let balance_of_callback_param : balance_of_response list = List.map get_balance balance_of_param.requests in + let convert = fun ( r : balance_of_response) -> Layout.convert_to_right_comb(({request=Layout.convert_to_right_comb( r.request ); balance=r.balance} : balance_of_response_aux)) in + let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in + // sending back the processed map of balance requests/responses + let destination: (balance_of_response_michelson list) contract = balance_of_param.callback in + let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in + ([balance_of_response_operation], s) + +let total_supply(params, s: total_supply_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in + let token_ids : token_id list = p.token_ids in + // Modify the code below + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith(token_undefined) : total_supply_response) + in + let responses : total_supply_response list = List.map get_total_supply token_ids in + let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in + let ret : total_supply_response_michelson list = List.map convert responses in + let destination: (total_supply_response_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let token_metadata(params, s: token_metadata_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : token_metadata_param = Layout.convert_from_right_comb(params: token_metadata_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> v.metadata + | None -> (failwith(token_undefined) : token_metadata) + in + let responses : token_metadata list = List.map get_metadata token_ids in + let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in + let ret : token_metadata_michelson list = List.map convert responses in + let destination: (token_metadata_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let transfer(params, s: transfer_michelson list * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let apply_transfer = fun (l,i : ledger * transfer_michelson) -> + let t_aux : transfer_aux = Layout.convert_from_right_comb(i: transfer_michelson) in + let from_ : address = t_aux.from_ in + let result_ledger : ledger = if Tezos.sender = from_ or Tezos.sender = s.administrator then + if Set.mem {owner=from_;operator=Tezos.sender} s.operators then + let transfers : transfer_destination_michelson list = t_aux.txs in + let apply_transfer_destination = fun (acc,j : (ledger * transfer_destination_michelson)) -> + let transfer_destination : transfer_destination = Layout.convert_from_right_comb(j: transfer_destination_michelson) in + let tr_amount : nat = transfer_destination.amount in + let to_ : address = transfer_destination.to_ in + let tokenid : token_id = transfer_destination.token_id in + let temp_state_ledger : ledger = if tr_amount > 0n then + let enough_funds : bool = match Map.find_opt (from_,tokenid) acc with + | Some bal -> (bal.balance >= tr_amount) + | None -> false + in + if enough_funds then + let l_updated_from : ledger = match Map.find_opt (from_,tokenid) acc with + | Some bal -> Map.update (from_,tokenid) (Some {balance=abs(bal.balance - tr_amount)} ) acc + | None -> (failwith("should not arrive here") : ledger) + in + let l_updated_from_to : ledger = match Map.find_opt (to_,tokenid) l_updated_from with + | Some bal -> Map.update (to_,tokenid) (Some {balance=bal.balance + tr_amount}) l_updated_from + | None -> Map.add (to_,tokenid) {balance=tr_amount} l_updated_from + in + l_updated_from_to + else + (failwith(insufficient_balance) : ledger) + else + (failwith("transferring nothing !") : ledger) + in + temp_state_ledger + in + let result : ledger = List.fold apply_transfer_destination transfers l in + result + else + (failwith(not_operator) : ledger) + else + (failwith(not_owner) : ledger) + in + result_ledger + in + let new_ledger : ledger = List.fold apply_transfer params s.ledger in + (([] : operation list), new_ledger) + + +let main (p,s : entry_points * storage) : return = + match p with + | MyTotal_supply p -> total_supply (p,s) + | MyBalance_of p -> balance_of (p,s) + | MyToken_metadata p -> token_metadata (p,s) + | Set_pause p -> set_pause (p,s) + | Set_administrator p -> set_administrator (p,s) + | MyTransfer l -> transfer (l, s) \ No newline at end of file From 650e30b22791414fb7fde18d76bda8c632f0d0f2 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Fri, 12 Jun 2020 12:19:42 +0200 Subject: [PATCH 03/16] [Cameligo] FA2 part1 fungible-multiasset implementation + exercise --- .../Chapters/Camel/ChapterFA20/caller.mligo | 31 ++- .../Chapters/Camel/ChapterFA20/course.md | 9 +- .../Chapters/Camel/ChapterFA20/exercise.mligo | 98 ++++++--- .../Camel/ChapterFA20/fungible_token.mligo | 194 ++++++++++++++++++ .../pages/Chapters/Camel/ChapterFA20/index.ts | 8 +- .../Chapters/Camel/ChapterFA20/solution.mligo | 96 ++++++--- .../Camel/ChapterFA20Permission/course.md | 30 ++- .../Camel/ChapterFA20Permission/exercise.cmd | 5 + 8 files changed, 401 insertions(+), 70 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/fungible_token.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo index a4d6843..e04f9b9 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo @@ -2,6 +2,8 @@ #include "tzip/proposals/tzip-12/lib/fa2_behaviors.mligo" type storage = { + fa2_registry : fa2_registry; + descriptor : permissions_descriptor; rep : balance_of_response_michelson list } @@ -14,10 +16,10 @@ type receive_balance_of_param = balance_of_response_michelson list let request_balance (req, s : (request_balance_of_param * storage)) : operation list * storage = // Get the TZIP-12 contract instance 'from' the chain - let token_contract_balance_entrypoint_opt : balance_of_param contract option = Tezos.get_entrypoint_opt "%balance_of" req.at in - let token_contract_balance_entrypoint : balance_of_param contract = match (token_contract_balance_entrypoint_opt) with + let token_contract_balance_entrypoint_opt : balance_of_param_michelson contract option = Tezos.get_entrypoint_opt "%balance_of" req.at in + let token_contract_balance_entrypoint : balance_of_param_michelson contract = match (token_contract_balance_entrypoint_opt) with | Some (ci) -> ci - | None -> (failwith("Entrypoint not found"): balance_of_param contract) + | None -> (failwith("Entrypoint not found"): balance_of_param_michelson contract) in // Callback (contract) for Balance_of will be the current contract's Receive_balance entrypoint @@ -28,14 +30,17 @@ let request_balance (req, s : (request_balance_of_param * storage)) : operation in //let balance_of_callback_contract : receive_balance_of_param contract = get_entrypoint("%receive_balance", Tezos.self_address) in // Set up the parameter w/ data required for the Balance_of entrypoint - let balance_of_operation_param : balance_of_param = { - requests = req.requests; + let convert = fun (i : balance_of_request) -> Layout.convert_to_right_comb(i) in + let request_michelson : balance_of_request_michelson list = List.map convert req.requests in + let balance_of_operation_param : balance_of_param_aux = { + requests = request_michelson; callback = balance_of_callback_contract; } in + let reqPayload : balance_of_param_michelson = Layout.convert_to_right_comb(balance_of_operation_param) in // Forge an internal transaction to the TZIP-12 contract // parametrised by the prieviously prepared `balance_of_operation_param` // Note: We're sending 0mutez as part of this transaction - let balance_of_operation : operation = Tezos.transaction balance_of_operation_param 0mutez token_contract_balance_entrypoint in + let balance_of_operation : operation = Tezos.transaction reqPayload 0mutez token_contract_balance_entrypoint in ([ balance_of_operation ], s) let receive_balance (received, s: (receive_balance_of_param * storage)) : operation list * storage = @@ -45,9 +50,23 @@ let receive_balance (received, s: (receive_balance_of_param * storage)) : operat type entry_points = | Request_balance of request_balance_of_param | Receive_balance of receive_balance_of_param + | Tokens_transferred_hook of transfer_descriptor_param_michelson + | Register_with_fa2 of fa2_with_hook_entry_points contract let main (param, s : entry_points * storage) : (operation list) * storage = match param with + | Tokens_transferred_hook pm -> + let p = transfer_descriptor_param_from_michelson pm in + let u = validate_hook_call (p.fa2, s.fa2_registry) in + let ops = standard_transfer_hook ( + {ligo_param = p; michelson_param = pm}, s.descriptor) in + ops, s + + | Register_with_fa2 fa2 -> + let op , new_registry = register_with_fa2 (fa2, s.descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + | Request_balance request_balance_of_param -> request_balance (request_balance_of_param, s) | Receive_balance receive_balance_of_param -> receive_balance (receive_balance_of_param, s) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md index fa34671..79e4c63 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md @@ -235,14 +235,13 @@ Error mnemonic - Description ## Your mission +We are working on a fungible/multi-asset token compliant with the FA2 standard. We want you to complete the existing implementation of token. The Total_supply entry point is not yet implemented , please finish the job ! - We want you to complete the existing implementation. The Total_supply entry point is not yet implemented , please finish the job ! +1 - Modify the *get_total_supply* lambda function in order to retrieve the total_supply information related to the given *token_id* list. -1 - Modify the *get_total_supply* function in order to retrieve the total_supply information related to the given *token_id* list. +2 - the *get_total_supply* lambda function For each given token_id, find the given *token_id* in the *tokens* map and retrieve the *total_supply* associated to a given *token_id* in the *tokens* map. -2 -You must find each token in the *tokens* map. - -3 -If a given token_id is found then the function *get_total_supply* must return a *total_supply_response* record for each *token_id*. As seen in the interface the *total_supply_response* record contains *token_id* and *total_supply* fields. (use v as temporary variable for the match with instruction) +3 -If a given token_id is found then the function *get_total_supply* must return a *total_supply_response* record for each given *token_id*. As seen in the interface the *total_supply_response* record contains *token_id* and *total_supply* fields. (use v as temporary variable for the match with instruction) 4 -If a given token_id is not found then the function *get_total_supply* must throw an exception with the predefined error messsage *token_undefined*. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo index 41dacbe..9b61bd0 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo @@ -12,17 +12,13 @@ type tokens = { metadata : token_metadata; } -type operator_ = { - owner : address; - operator : address; -} - type storage = { paused : bool; ledger : ledger; - tokens : (nat,tokens) map; - operators : operator_ set; + tokens : (token_id,tokens) map; + operators : operator_param set; administrator : address; + permissions_descriptor : permissions_descriptor_aux; } type return = (operation list * storage) @@ -30,10 +26,8 @@ type return = (operation list * storage) type entry_points = | Set_pause of bool | Set_administrator of address - | MyBalance_of of balance_of_param - | MyTotal_supply of total_supply_param_michelson - | MyToken_metadata of token_metadata_param_michelson - | MyTransfer of transfer_michelson list + +type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or let set_pause(param,s : bool * storage): return = if Tezos.sender = s.administrator then @@ -47,16 +41,19 @@ let set_administrator(param,s : address * storage): return = else (failwith("only admin can do it") : return) -let balance_of (balance_of_param, s : balance_of_param * storage) : return = - let get_balance = fun ( i : balance_of_request) -> match Map.find_opt (i.owner,i.token_id) s.ledger with - | Some e -> { request = i ; balance =e.balance } - | None -> (failwith("unknown owner") : balance_of_response) +let balance_of (param, s : balance_of_param_michelson * storage) : return = + let param_bo_aux : balance_of_param_aux = Layout.convert_from_right_comb(param: balance_of_param_michelson) in + let get_balance = fun ( i : balance_of_request_michelson) -> + let bor : balance_of_request = Layout.convert_from_right_comb(i) in + match Map.find_opt (bor.owner,bor.token_id) s.ledger with + | Some e -> { request = Layout.convert_to_right_comb(bor) ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response_aux) in - let balance_of_callback_param : balance_of_response list = List.map get_balance balance_of_param.requests in - let convert = fun ( r : balance_of_response) -> Layout.convert_to_right_comb(({request=Layout.convert_to_right_comb( r.request ); balance=r.balance} : balance_of_response_aux)) in + let balance_of_callback_param : balance_of_response_aux list = List.map get_balance param_bo_aux.requests in + let convert = fun ( r : balance_of_response_aux) -> Layout.convert_to_right_comb(r) in let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in // sending back the processed map of balance requests/responses - let destination: (balance_of_response_michelson list) contract = balance_of_param.callback in + let destination: (balance_of_response_michelson list) contract = param_bo_aux.callback in let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in ([balance_of_response_operation], s) @@ -65,9 +62,11 @@ let total_supply(params, s: total_supply_param_michelson * storage) : return = (failwith("contract in pause") : return) else let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in - let token_ids : token_id list = p.token_ids in + let token_ids : token_id list = p.token_ids in // Modify the code below - let get_total_supply = fun ( i : token_id) -> + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith(token_undefined) : total_supply_response) in let responses : total_supply_response list = List.map get_total_supply token_ids in let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in @@ -140,14 +139,57 @@ let transfer(params, s: transfer_michelson list * storage) : return = result_ledger in let new_ledger : ledger = List.fold apply_transfer params s.ledger in - (([] : operation list), new_ledger) + (([] : operation list), {s with ledger=new_ledger}) + +let update_operators (params,s : (update_operator_michelson list * storage)) : return = + if Tezos.sender <> s.administrator then + (failwith("operators can only be modified by the admin") : return) + else + let convert = fun (i : update_operator_michelson) -> (Layout.convert_from_right_comb(i) : update_operator_aux) in + let params_aux_list : update_operator_aux list = List.map convert params in + let apply_order = fun (acc,j : operator_param set * update_operator_aux) -> + match j with + | Add_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.add p acc + else + (failwith("notautorized !!!! ") : operator_param set) + | Remove_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.remove p acc + else + (failwith("notautorized !!!! ") : operator_param set) + in + let new_operators : operator_param set = List.fold apply_order params_aux_list s.operators in + (([] : operation list), {s with operators=new_operators}) + +let is_operator(params,s : (is_operator_param_michelson * storage)) : return = + let p : is_operator_param_aux = Layout.convert_from_right_comb(params) in + let op_param : operator_param = Layout.convert_from_right_comb(p.operator) in + let response_aux : is_operator_response_aux = {operator=p.operator;is_operator=Set.mem op_param s.operators} in + let response : is_operator_response_michelson = Layout.convert_to_right_comb(response_aux) in + let destination: (is_operator_response_michelson) contract = p.callback in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) +let send_permissions_descriptor(param,s : (permissions_descriptor_michelson contract * storage)) : return = + let response : permissions_descriptor_michelson = Layout.convert_to_right_comb(s.permissions_descriptor) in + let destination: permissions_descriptor_michelson contract = param in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) -let main (p,s : entry_points * storage) : return = - match p with - | MyTotal_supply p -> total_supply (p,s) - | MyBalance_of p -> balance_of (p,s) - | MyToken_metadata p -> token_metadata (p,s) +let main (param,s : total_entry_points * storage) : return = + match param with + | M_left fa2_ep -> (match fa2_ep with + | Transfer l -> transfer (l, s) + | Balance_of p -> balance_of (p, s) + | Total_supply p -> total_supply (p,s) + | Token_metadata p -> token_metadata (p,s) + | Permissions_descriptor callback -> send_permissions_descriptor (callback, s) + | Update_operators l -> update_operators (l,s) + | Is_operator o -> is_operator (o,s)) + | M_right specific_ep -> (match specific_ep with | Set_pause p -> set_pause (p,s) - | Set_administrator p -> set_administrator (p,s) - | MyTransfer l -> transfer (l, s) \ No newline at end of file + | Set_administrator p -> set_administrator (p,s)) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/fungible_token.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/fungible_token.mligo new file mode 100644 index 0000000..2bfbe83 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/fungible_token.mligo @@ -0,0 +1,194 @@ +#include "tzip/proposals/tzip-12/fa2_interface.mligo" +#include "tzip/proposals/tzip-12/fa2_errors.mligo" + +type entity = { + balance : nat +} +type entity_key = address * token_id +type ledger = (entity_key, entity) map + +type tokens = { + total_supply : nat; + metadata : token_metadata; +} + +type storage = { + paused : bool; + ledger : ledger; + tokens : (token_id,tokens) map; + operators : operator_param set; + administrator : address; + permissions_descriptor : permissions_descriptor_aux; +} + +type return = (operation list * storage) + +type entry_points = + | Set_pause of bool + | Set_administrator of address + +type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or + +let set_pause(param,s : bool * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with paused=param }) + else + (failwith("only admin can do it") : return) + +let set_administrator(param,s : address * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with administrator=param }) + else + (failwith("only admin can do it") : return) + +let balance_of (param, s : balance_of_param_michelson * storage) : return = + let param_bo_aux : balance_of_param_aux = Layout.convert_from_right_comb(param: balance_of_param_michelson) in + let get_balance = fun ( i : balance_of_request_michelson) -> + let bor : balance_of_request = Layout.convert_from_right_comb(i) in + match Map.find_opt (bor.owner,bor.token_id) s.ledger with + | Some e -> { request = Layout.convert_to_right_comb(bor) ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response_aux) + in + let balance_of_callback_param : balance_of_response_aux list = List.map get_balance param_bo_aux.requests in + let convert = fun ( r : balance_of_response_aux) -> Layout.convert_to_right_comb(r) in + let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in + // sending back the processed map of balance requests/responses + let destination: (balance_of_response_michelson list) contract = param_bo_aux.callback in + let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in + ([balance_of_response_operation], s) + +let total_supply(params, s: total_supply_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith(token_undefined) : total_supply_response) + in + let responses : total_supply_response list = List.map get_total_supply token_ids in + let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in + let ret : total_supply_response_michelson list = List.map convert responses in + let destination: (total_supply_response_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let token_metadata(params, s: token_metadata_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : token_metadata_param = Layout.convert_from_right_comb(params: token_metadata_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> v.metadata + | None -> (failwith("unknown token_id") : token_metadata) + in + let responses : token_metadata list = List.map get_metadata token_ids in + let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in + let ret : token_metadata_michelson list = List.map convert responses in + let destination: (token_metadata_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let transfer(params, s: transfer_michelson list * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let apply_transfer = fun (l,i : ledger * transfer_michelson) -> + let t_aux : transfer_aux = Layout.convert_from_right_comb(i: transfer_michelson) in + let from_ : address = t_aux.from_ in + let result_ledger : ledger = if Tezos.sender = from_ or Tezos.sender = s.administrator then + if Set.mem {owner=from_;operator=Tezos.sender} s.operators then + let transfers : transfer_destination_michelson list = t_aux.txs in + let apply_transfer_destination = fun (acc,j : (ledger * transfer_destination_michelson)) -> + let transfer_destination : transfer_destination = Layout.convert_from_right_comb(j: transfer_destination_michelson) in + let tr_amount : nat = transfer_destination.amount in + let to_ : address = transfer_destination.to_ in + let tokenid : token_id = transfer_destination.token_id in + let temp_state_ledger : ledger = if tr_amount > 0n then + let enough_funds : bool = match Map.find_opt (from_,tokenid) acc with + | Some bal -> (bal.balance >= tr_amount) + | None -> false + in + if enough_funds then + let l_updated_from : ledger = match Map.find_opt (from_,tokenid) acc with + | Some bal -> Map.update (from_,tokenid) (Some {balance=abs(bal.balance - tr_amount)} ) acc + | None -> (failwith("should not arrive here") : ledger) + in + let l_updated_from_to : ledger = match Map.find_opt (to_,tokenid) l_updated_from with + | Some bal -> Map.update (to_,tokenid) (Some {balance=bal.balance + tr_amount}) l_updated_from + | None -> Map.add (to_,tokenid) {balance=tr_amount} l_updated_from + in + l_updated_from_to + else + (failwith(insufficient_balance) : ledger) + else + (failwith("transferring nothing !") : ledger) + in + temp_state_ledger + in + let result : ledger = List.fold apply_transfer_destination transfers l in + result + else + (failwith(not_operator) : ledger) + else + (failwith(not_owner) : ledger) + in + result_ledger + in + let new_ledger : ledger = List.fold apply_transfer params s.ledger in + (([] : operation list), {s with ledger=new_ledger}) + +let update_operators (params,s : (update_operator_michelson list * storage)) : return = + if Tezos.sender <> s.administrator then + (failwith("operators can only be modified by the admin") : return) + else + let convert = fun (i : update_operator_michelson) -> (Layout.convert_from_right_comb(i) : update_operator_aux) in + let params_aux_list : update_operator_aux list = List.map convert params in + let apply_order = fun (acc,j : operator_param set * update_operator_aux) -> + match j with + | Add_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.add p acc + else + (failwith("notautorized !!!! ") : operator_param set) + | Remove_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.remove p acc + else + (failwith("notautorized !!!! ") : operator_param set) + in + let new_operators : operator_param set = List.fold apply_order params_aux_list s.operators in + (([] : operation list), {s with operators=new_operators}) + +let is_operator(params,s : (is_operator_param_michelson * storage)) : return = + let p : is_operator_param_aux = Layout.convert_from_right_comb(params) in + let op_param : operator_param = Layout.convert_from_right_comb(p.operator) in + let response_aux : is_operator_response_aux = {operator=p.operator;is_operator=Set.mem op_param s.operators} in + let response : is_operator_response_michelson = Layout.convert_to_right_comb(response_aux) in + let destination: (is_operator_response_michelson) contract = p.callback in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let send_permissions_descriptor(param,s : (permissions_descriptor_michelson contract * storage)) : return = + let response : permissions_descriptor_michelson = Layout.convert_to_right_comb(s.permissions_descriptor) in + let destination: permissions_descriptor_michelson contract = param in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let main (param,s : total_entry_points * storage) : return = + match param with + | M_left fa2_ep -> (match fa2_ep with + | Transfer l -> transfer (l, s) + | Balance_of p -> balance_of (p, s) + | Total_supply p -> total_supply (p,s) + | Token_metadata p -> token_metadata (p,s) + | Permissions_descriptor callback -> send_permissions_descriptor (callback, s) + | Update_operators l -> update_operators (l,s) + | Is_operator o -> is_operator (o,s)) + | M_right specific_ep -> (match specific_ep with + | Set_pause p -> set_pause (p,s) + | Set_administrator p -> set_administrator (p,s)) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts index 5afd766..504cda9 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts @@ -7,5 +7,11 @@ import exercise from "!raw-loader!./exercise.mligo"; /* eslint import/no-webpack-loader-syntax: off */ // @ts-ignore import solution from "!raw-loader!./solution.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support1 from "!raw-loader!./fungible_token.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support2 from "!raw-loader!./caller.mligo"; -export const data = { course, exercise, solution }; +export const data = { course, exercise, solution, support1, support2 }; diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo index b49aa9a..5f3484f 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo @@ -12,17 +12,13 @@ type tokens = { metadata : token_metadata; } -type operator_ = { - owner : address; - operator : address; -} - type storage = { paused : bool; ledger : ledger; - tokens : (nat,tokens) map; - operators : operator_ set; + tokens : (token_id,tokens) map; + operators : operator_param set; administrator : address; + permissions_descriptor : permissions_descriptor_aux; } type return = (operation list * storage) @@ -30,10 +26,8 @@ type return = (operation list * storage) type entry_points = | Set_pause of bool | Set_administrator of address - | MyBalance_of of balance_of_param - | MyTotal_supply of total_supply_param_michelson - | MyToken_metadata of token_metadata_param_michelson - | MyTransfer of transfer_michelson list + +type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or let set_pause(param,s : bool * storage): return = if Tezos.sender = s.administrator then @@ -47,16 +41,19 @@ let set_administrator(param,s : address * storage): return = else (failwith("only admin can do it") : return) -let balance_of (balance_of_param, s : balance_of_param * storage) : return = - let get_balance = fun ( i : balance_of_request) -> match Map.find_opt (i.owner,i.token_id) s.ledger with - | Some e -> { request = i ; balance =e.balance } - | None -> (failwith("unknown owner") : balance_of_response) +let balance_of (param, s : balance_of_param_michelson * storage) : return = + let param_bo_aux : balance_of_param_aux = Layout.convert_from_right_comb(param: balance_of_param_michelson) in + let get_balance = fun ( i : balance_of_request_michelson) -> + let bor : balance_of_request = Layout.convert_from_right_comb(i) in + match Map.find_opt (bor.owner,bor.token_id) s.ledger with + | Some e -> { request = Layout.convert_to_right_comb(bor) ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response_aux) in - let balance_of_callback_param : balance_of_response list = List.map get_balance balance_of_param.requests in - let convert = fun ( r : balance_of_response) -> Layout.convert_to_right_comb(({request=Layout.convert_to_right_comb( r.request ); balance=r.balance} : balance_of_response_aux)) in + let balance_of_callback_param : balance_of_response_aux list = List.map get_balance param_bo_aux.requests in + let convert = fun ( r : balance_of_response_aux) -> Layout.convert_to_right_comb(r) in let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in // sending back the processed map of balance requests/responses - let destination: (balance_of_response_michelson list) contract = balance_of_param.callback in + let destination: (balance_of_response_michelson list) contract = param_bo_aux.callback in let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in ([balance_of_response_operation], s) @@ -67,9 +64,7 @@ let total_supply(params, s: total_supply_param_michelson * storage) : return = let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in let token_ids : token_id list = p.token_ids in // Modify the code below - let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with - | Some v -> { token_id = i ; total_supply =v.total_supply } - | None -> (failwith(token_undefined) : total_supply_response) + let get_total_supply = in let responses : total_supply_response list = List.map get_total_supply token_ids in let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in @@ -142,14 +137,57 @@ let transfer(params, s: transfer_michelson list * storage) : return = result_ledger in let new_ledger : ledger = List.fold apply_transfer params s.ledger in - (([] : operation list), new_ledger) + (([] : operation list), {s with ledger=new_ledger}) + +let update_operators (params,s : (update_operator_michelson list * storage)) : return = + if Tezos.sender <> s.administrator then + (failwith("operators can only be modified by the admin") : return) + else + let convert = fun (i : update_operator_michelson) -> (Layout.convert_from_right_comb(i) : update_operator_aux) in + let params_aux_list : update_operator_aux list = List.map convert params in + let apply_order = fun (acc,j : operator_param set * update_operator_aux) -> + match j with + | Add_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.add p acc + else + (failwith("notautorized !!!! ") : operator_param set) + | Remove_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.remove p acc + else + (failwith("notautorized !!!! ") : operator_param set) + in + let new_operators : operator_param set = List.fold apply_order params_aux_list s.operators in + (([] : operation list), {s with operators=new_operators}) + +let is_operator(params,s : (is_operator_param_michelson * storage)) : return = + let p : is_operator_param_aux = Layout.convert_from_right_comb(params) in + let op_param : operator_param = Layout.convert_from_right_comb(p.operator) in + let response_aux : is_operator_response_aux = {operator=p.operator;is_operator=Set.mem op_param s.operators} in + let response : is_operator_response_michelson = Layout.convert_to_right_comb(response_aux) in + let destination: (is_operator_response_michelson) contract = p.callback in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) +let send_permissions_descriptor(param,s : (permissions_descriptor_michelson contract * storage)) : return = + let response : permissions_descriptor_michelson = Layout.convert_to_right_comb(s.permissions_descriptor) in + let destination: permissions_descriptor_michelson contract = param in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) -let main (p,s : entry_points * storage) : return = - match p with - | MyTotal_supply p -> total_supply (p,s) - | MyBalance_of p -> balance_of (p,s) - | MyToken_metadata p -> token_metadata (p,s) +let main (param,s : total_entry_points * storage) : return = + match param with + | M_left fa2_ep -> (match fa2_ep with + | Transfer l -> transfer (l, s) + | Balance_of p -> balance_of (p, s) + | Total_supply p -> total_supply (p,s) + | Token_metadata p -> token_metadata (p,s) + | Permissions_descriptor callback -> send_permissions_descriptor (callback, s) + | Update_operators l -> update_operators (l,s) + | Is_operator o -> is_operator (o,s)) + | M_right specific_ep -> (match specific_ep with | Set_pause p -> set_pause (p,s) - | Set_administrator p -> set_administrator (p,s) - | MyTransfer l -> transfer (l, s) \ No newline at end of file + | Set_administrator p -> set_administrator (p,s)) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md index c97051d..271a062 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md @@ -8,10 +8,26 @@ The FA2 standard proposes a *unified token contract interface* that accommodates In this chapter we will focus on _Operators_ and _Permissions_. +## Entry points + +Token contract implementing the FA2 standard MUST have the following entry points. + +``` +type fa2_entry_points = + +| Transfer of transfer list +| Balance_of of balance_of_param +| Total_supply of total_supply_param +| Token_metadata of token_metadata_param +| Permissions_descriptor of permissions_descriptor contract +| Update_operators of update_operator list +| Is_operator of is_operator_param +``` ### Operators #### Definition +_Operator_ can be seen as delegate role. _Operator_ is a Tezos address that initiates token transfer operation on behalf of the owner. _Owner_ is a Tezos address which can hold tokens. @@ -160,5 +176,17 @@ Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) ## Your mission -1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. +Our NFT "token" is almost ready but to allow a new rule. We need A to transfert a token taken from B account and send it to C account. + +1- First we have to set the right operator policy to authorize delegation when deploying the contract. We want you to prepare the initial state of storage. Write the _ligo compile-storage_ command for the *token* contract with following recommandations : + * operator transfer is authorized, + * A account has a balance set to 100 + * B account has a balance set to 100 + * C account has a balance set to 100 + * D is the administrator of the contract + + +2- Write the _ligo dry-run_ command for authorizing A to transfer token taken from B account, transaction emitted by D. (reuse the storage you made on step 1) + +3- Write the _ligo dry-run_ command for simulating the transfer of 1 mutez from B'account to c's account, transaction emitted by A. You will have to modify the storage to take step 2 into account. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd new file mode 100644 index 0000000..c9b5de0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd @@ -0,0 +1,5 @@ +//let new_perm : permissions_descriptor = {operator=Owner_or_operator_transfer; receiver=Owner_no_hook; sender=Owner_no_hook; custom=(None : custom_permission_policy option) } + +ligo compile-storage shiptoken.mligo main '' +ligo dry-run shiptoken.ligo 'Update_operators([{owner=TZ1;operator=TZ1}])' '' +ligo dry-run --sender=A shiptoken.ligo 'Transfer(B,C,token_id)' \ No newline at end of file From 7687e646b2a2a78cf7a44faee3536b79e443e524 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Fri, 12 Jun 2020 18:39:28 +0200 Subject: [PATCH 04/16] [Cameligo] FA2_chapter: implementation of a non-fungible-token --- .../Camel/ChapterFA20Permission/course.md | 2 +- .../Camel/ChapterFA20Permission/exercise.cmd | 4 +- .../non_fungible_token.mligo | 255 ++++++++++++++++++ 3 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md index 271a062..8c96355 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md @@ -175,7 +175,7 @@ Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) ## Your mission - +We are working on a non_fungible/multi-asset token. Our NFT "token" is almost ready but to allow a new rule. We need A to transfert a token taken from B account and send it to C account. 1- First we have to set the right operator policy to authorize delegation when deploying the contract. We want you to prepare the initial state of storage. Write the _ligo compile-storage_ command for the *token* contract with following recommandations : diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd index c9b5de0..bb78a16 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd @@ -1,5 +1,3 @@ -//let new_perm : permissions_descriptor = {operator=Owner_or_operator_transfer; receiver=Owner_no_hook; sender=Owner_no_hook; custom=(None : custom_permission_policy option) } - -ligo compile-storage shiptoken.mligo main '' +ligo compile-storage non_fungible_token.mligo main '' ligo dry-run shiptoken.ligo 'Update_operators([{owner=TZ1;operator=TZ1}])' '' ligo dry-run --sender=A shiptoken.ligo 'Transfer(B,C,token_id)' \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo new file mode 100644 index 0000000..e845d6a --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo @@ -0,0 +1,255 @@ +#include "tzip/proposals/tzip-12/fa2_interface.mligo" +#include "tzip/proposals/tzip-12/fa2_errors.mligo" + +type entity = { + code : string; + name : string; +} +type entity_id = nat +type entity_key = (entity_id * token_id) +type entities = (entity_key, entity) map +type entityIndexToOwner = (entity_key, address) map +type owner_entities = (address, entity_key set) map +//type owner_entities_count = (address, nat) map + +type token = { + total_supply : nat; + metadata : token_metadata; +} + +type storage = { + paused : bool; + entities : entities; + entity_owner : entityIndexToOwner; + owner_entities : owner_entities; + tokens : (token_id,token) map; + operators : operator_param set; + administrator : address; + permissions_descriptor : permissions_descriptor_aux; +} + +type return = (operation list * storage) + +type entry_points = + | Set_pause of bool + | Set_administrator of address + | Mint of (entity * address * token_id) + +type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or + +let set_pause(param,s : bool * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with paused=param }) + else + (failwith("only admin can do it") : return) + +let set_administrator(param,s : address * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with administrator=param }) + else + (failwith("only admin can do it") : return) + +let mint (param,s : (entity * address * token_id) * storage) : return = + if Tezos.sender = s.administrator then + let new_ent : entity = param.0 in + let owner : address = param.1 in + let tokenid : token_id = param.2 in + // NEVER burn an entity or entity_id will be provided an existing id (mint burn mint) + let newid : entity_id = match Map.find_opt tokenid s.tokens with + | Some tok -> tok.total_supply + 1n + | None -> (failwith("unknown token_id") : nat) + in + // entities[newid] = new_ent + let new_entities : entities = match Map.find_opt (newid,tokenid) s.entities with + | Some v -> (failwith("token already exist") : entities) + | None -> Map.add (newid,tokenid) new_ent s.entities + in + // entity_owner[(newid,tokenid)] = owner + let new_entityIndexToOwner : entityIndexToOwner = match Map.find_opt (newid,tokenid) s.entity_owner with + | Some addr -> (failwith("already owned") : entityIndexToOwner) + | None -> Map.add (newid,tokenid) owner s.entity_owner + in + // owner_entities[owner] = Set.add (newid,tokenid) owner_entities[owner] + let new_owner_entities : owner_entities = match Map.find_opt owner s.owner_entities with + | Some et_s -> Map.update owner (Some (Set.add (newid,tokenid) et_s)) s.owner_entities + | None -> Map.add owner (Set.add (newid,tokenid) Set.empty) s.owner_entities + in + // total_supply + 1 + let new_tokens : (token_id,token) map = match Map.find_opt tokenid s.tokens with + | Some tok -> Map.update tokenid (Some {tok with total_supply=tok.total_supply+1n}) s.tokens + | None -> (failwith("unknown token_id") : (token_id,token) map) + in + + (([] : operation list), { s with entities=new_entities; entity_owner=new_entityIndexToOwner; owner_entities=new_owner_entities; tokens=new_tokens}) + else + (failwith("only admin can do it") : return) + +let balance_of (param, s : balance_of_param_michelson * storage) : return = + let param_bo_aux : balance_of_param_aux = Layout.convert_from_right_comb(param: balance_of_param_michelson) in + let get_balance = fun ( i : balance_of_request_michelson) -> + let bor : balance_of_request = Layout.convert_from_right_comb(i) in + match Map.find_opt bor.owner s.owner_entities with + | Some et_s -> + let requested_token_id : token_id = bor.token_id in + // loop on et_s set of (entity_id * token_id) + let compute_balance = fun (acc,et : (nat * entity_key)) -> + if et.1 = requested_token_id then + acc + 1n + else + acc + in + let computed_balance : nat = Set.fold compute_balance et_s 0n in + { request = Layout.convert_to_right_comb(bor) ; balance =computed_balance } + | None -> (failwith("unknown owner") : balance_of_response_aux) + in + let balance_of_callback_param : balance_of_response_aux list = List.map get_balance param_bo_aux.requests in + let convert = fun ( r : balance_of_response_aux) -> Layout.convert_to_right_comb(r) in + let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in + // sending back the processed map of balance requests/responses + let destination: (balance_of_response_michelson list) contract = param_bo_aux.callback in + let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in + ([balance_of_response_operation], s) + +let total_supply(params, s: total_supply_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith("unknown token_id") : total_supply_response) + in + let responses : total_supply_response list = List.map get_total_supply token_ids in + let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in + let ret : total_supply_response_michelson list = List.map convert responses in + let destination: (total_supply_response_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let token_metadata(params, s: token_metadata_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : token_metadata_param = Layout.convert_from_right_comb(params: token_metadata_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> v.metadata + | None -> (failwith("unknown token_id") : token_metadata) + in + let responses : token_metadata list = List.map get_metadata token_ids in + let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in + let ret : token_metadata_michelson list = List.map convert responses in + let destination: (token_metadata_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let transfer(params, s: transfer_michelson list * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let apply_transfer = fun (eo_oe,i : (entityIndexToOwner * owner_entities) * transfer_michelson) -> + let t_aux : transfer_aux = Layout.convert_from_right_comb(i: transfer_michelson) in + let from_ : address = t_aux.from_ in + let result_ledger : (entityIndexToOwner * owner_entities) = + if Tezos.sender = from_ or Tezos.sender = s.administrator or Set.mem {owner=from_;operator=Tezos.sender} s.operators then + let transfers : transfer_destination_michelson list = t_aux.txs in + let apply_transfer_destination = fun (acc,j : ((entityIndexToOwner * owner_entities) * transfer_destination_michelson)) -> + let transfer_destination : transfer_destination = Layout.convert_from_right_comb(j: transfer_destination_michelson) in + let to_ : address = transfer_destination.to_ in + let tokenid : token_id = transfer_destination.token_id in + + // hack + let transfered_entity : nat = transfer_destination.amount in + + let current_entity_owner : entityIndexToOwner = acc.0 in + let current_owner_entities : owner_entities = acc.1 in + + //asset(entity_owner[entity_id] = from) + let assert_owner_from : bool = match Map.find_opt (transfered_entity,tokenid) current_entity_owner with + | None -> (failwith("entity does not exist") : bool) + | Some ownr -> ownr = from_ + in + if assert_owner_from then + //entity_owner[entity_id] = to + let update_entity_owner : entityIndexToOwner = Map.update (transfered_entity,tokenid) (Some to_) current_entity_owner in + + let owner_entities_from : entity_key set = match Map.find_opt from_ current_owner_entities with + | None -> (failwith(not_owner) : entity_key set) + | Some et_s -> Set.remove (transfered_entity,tokenid) et_s + in + let updated_owner_entities : owner_entities = Map.update from_ (Some owner_entities_from) current_owner_entities in + let owner_entities_to : entity_key set = match Map.find_opt to_ updated_owner_entities with + | None -> (failwith(not_owner) : entity_key set) + | Some et_s -> Set.add (transfered_entity,tokenid) et_s + in + let updated_owner_entities2 : owner_entities = Map.update to_ (Some owner_entities_to) updated_owner_entities in + (update_entity_owner, updated_owner_entities2) + else + (failwith(not_owner) : (entityIndexToOwner * owner_entities)) + in + let result : (entityIndexToOwner * owner_entities) = List.fold apply_transfer_destination transfers eo_oe in + result + else + (failwith(not_owner ^ not_operator) : (entityIndexToOwner * owner_entities)) + in + result_ledger + in + let new_ledger : (entityIndexToOwner * owner_entities) = List.fold apply_transfer params (s.entity_owner, s.owner_entities) in + (([] : operation list), {s with entity_owner=new_ledger.0;owner_entities=new_ledger.1}) + +let update_operators (params,s : (update_operator_michelson list * storage)) : return = + if Tezos.sender <> s.administrator then + (failwith("operators can only be modified by the admin") : return) + else + let convert = fun (i : update_operator_michelson) -> (Layout.convert_from_right_comb(i) : update_operator_aux) in + let params_aux_list : update_operator_aux list = List.map convert params in + let apply_order = fun (acc,j : operator_param set * update_operator_aux) -> + match j with + | Add_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.add p acc + else + (failwith("notautorized !!!! ") : operator_param set) + | Remove_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.remove p acc + else + (failwith("notautorized !!!! ") : operator_param set) + in + let new_operators : operator_param set = List.fold apply_order params_aux_list s.operators in + (([] : operation list), {s with operators=new_operators}) + +let is_operator(params,s : (is_operator_param_michelson * storage)) : return = + let p : is_operator_param_aux = Layout.convert_from_right_comb(params) in + let op_param : operator_param = Layout.convert_from_right_comb(p.operator) in + let response_aux : is_operator_response_aux = {operator=p.operator;is_operator=Set.mem op_param s.operators} in + let response : is_operator_response_michelson = Layout.convert_to_right_comb(response_aux) in + let destination: (is_operator_response_michelson) contract = p.callback in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let send_permissions_descriptor(param,s : (permissions_descriptor_michelson contract * storage)) : return = + let response : permissions_descriptor_michelson = Layout.convert_to_right_comb(s.permissions_descriptor) in + let destination: permissions_descriptor_michelson contract = param in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let main (param,s : total_entry_points * storage) : return = + match param with + | M_left fa2_ep -> (match fa2_ep with + | Transfer l -> transfer (l, s) + | Balance_of p -> balance_of (p, s) + | Total_supply p -> total_supply (p,s) + | Token_metadata p -> token_metadata (p,s) + | Permissions_descriptor callback -> send_permissions_descriptor (callback, s) + | Update_operators l -> update_operators (l,s) + | Is_operator o -> is_operator (o,s) + ) + | M_right specific_ep -> (match specific_ep with + | Set_pause p -> set_pause (p,s) + | Set_administrator p -> set_administrator (p,s) + | Mint ent -> mint (ent,s) + ) From 51874d08b346a8cfffd0e965d967e408b660b4c1 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Fri, 19 Jun 2020 13:39:03 +0200 Subject: [PATCH 05/16] Fix some typos --- .../pages/Chapters/Camel/ChapterLambda/course.md | 13 +++++-------- .../pages/Chapters/Camel/ChapterMultisig/course.md | 2 +- .../pages/Chapters/Pascal/ChapterLambda/course.md | 14 ++++++-------- .../Chapters/Pascal/ChapterMultisig/course.md | 2 +- .../pages/Chapters/Reason/ChapterLambda/course.md | 14 ++++++-------- .../Chapters/Reason/ChapterMultisig/course.md | 2 +- 6 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterLambda/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterLambda/course.md index 0c785bf..ea84aa8 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterLambda/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterLambda/course.md @@ -10,10 +10,7 @@ Tezos as a public blockchain expects that contracts should have same behaviour f We call *antipattern* when a smart contract have special role (admin) or smart contract that may be evolving (changing rules of the smart contract). The need to modify the behaviour of a smart contract emerges when for exemple the law of the country has changed and you need to apply the same changes to the rules of your smart contract. -One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by - * *Versioning by re-emission* - * *Versioning by contract communication* : - * *Versioning by lambda* : +One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by migrating sstorage information through transactions, or by forcing the new contract to request storage data from from the old contract or by customizing the contract implementation. In this chapter we will focus on the third solution. ### Versioning by re-emission @@ -29,11 +26,11 @@ Versioning can be done by writing a single smart contract that can change its pr ## Lambda -So the idea is to : -* define an anonymous function in the storage which is called in entrypoint -* write an entrypoint that allow to change implementation of this anonymous function +Changing the behavior of a smart contract can be done by customizing the implementation through lambda's function. -Let's consider the "starmap" smart contract +So the idea is to define an anonymous function in the storage which is called in entrypoint and write an entrypoint that allow to change implementation of this anonymous function. + +Let's consider the "starmap" smart contract : ``` // starmap.ligo type coordinates = { x : int; y : int; z : int } diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterMultisig/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterMultisig/course.md index aad2c68..e397046 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterMultisig/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterMultisig/course.md @@ -12,7 +12,7 @@ When invoking a smart contract, an entrypoint is called and usually an action is The purpose of a multi-signature pattern is to execute an action when all preconditions has been verified. The action that need to be executed depends on the smart contract logic. The mutli-signature implementation can be done in a single contract with the smart contract logic or in a separated contract like a proxy contract (which emits transactions to the contract containg the logic). -### rules +### Rules The multi-signature pattern can be described with this set of rules : diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterLambda/course.md b/src/frontend/src/pages/Chapters/Pascal/ChapterLambda/course.md index ab665b8..ae9e2d8 100644 --- a/src/frontend/src/pages/Chapters/Pascal/ChapterLambda/course.md +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterLambda/course.md @@ -10,10 +10,7 @@ Tezos as a public blockchain expects that contracts should have same behaviour f We call *antipattern* when a smart contract have special role (admin) or smart contract that may be evolving (changing rules of the smart contract). The need to modify the behaviour of a smart contract emerges when for exemple the law of the country has changed and you need to apply the same changes to the rules of your smart contract. -One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by - * *Versioning by re-emission* - * *Versioning by contract communication* : - * *Versioning by lambda* : +One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by migrating sstorage information through transactions, or by forcing the new contract to request storage data from from the old contract or by customizing the contract implementation. In this chapter we will focus on the third solution. ### Versioning by re-emission @@ -29,11 +26,12 @@ Versioning can be done by writing a single smart contract that can change its pr ## Lambda -So the idea is to : -* define an anonymous function in the storage which is called in entrypoint -* write an entrypoint that allow to change implementation of this anonymous function +Changing the behavior of a smart contract can be done by customizing the implementation through lambda's function. + +So the idea is to define an anonymous function in the storage which is called in entrypoint and write an entrypoint that allow to change implementation of this anonymous function. + +Let's consider the "starmap" smart contract : -Let's consider the "starmap" smart contract ``` // starmap.ligo type coordinates is record [ x : int; y : int; z : int ] diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterMultisig/course.md b/src/frontend/src/pages/Chapters/Pascal/ChapterMultisig/course.md index be1f9ca..75993b9 100644 --- a/src/frontend/src/pages/Chapters/Pascal/ChapterMultisig/course.md +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterMultisig/course.md @@ -12,7 +12,7 @@ When invoking a smart contract, an entrypoint is called and usually an action is The purpose of a multi-signature pattern is to execute an action when all preconditions has been verified. The action that need to be executed depends on the smart contract logic. The mutli-signature implementation can be done in a single contract with the smart contract logic or in a separated contract like a proxy contract (which emits transactions to the contract containg the logic). -### rules +### Rules The multi-signature pattern can be described with this set of rules : diff --git a/src/frontend/src/pages/Chapters/Reason/ChapterLambda/course.md b/src/frontend/src/pages/Chapters/Reason/ChapterLambda/course.md index 396197a..cf287ce 100644 --- a/src/frontend/src/pages/Chapters/Reason/ChapterLambda/course.md +++ b/src/frontend/src/pages/Chapters/Reason/ChapterLambda/course.md @@ -10,10 +10,7 @@ Tezos as a public blockchain expects that contracts should have same behaviour f We call *antipattern* when a smart contract have special role (admin) or smart contract that may be evolving (changing rules of the smart contract). The need to modify the behaviour of a smart contract emerges when for exemple the law of the country has changed and you need to apply the same changes to the rules of your smart contract. -One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by - * *Versioning by re-emission* - * *Versioning by contract communication* : - * *Versioning by lambda* : +One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by migrating sstorage information through transactions, or by forcing the new contract to request storage data from from the old contract or by customizing the contract implementation. In this chapter we will focus on the third solution. ### Versioning by re-emission @@ -29,11 +26,12 @@ Versioning can be done by writing a single smart contract that can change its pr ## Lambda -So the idea is to : -* define an anonymous function in the storage which is called in entrypoint -* write an entrypoint that allow to change implementation of this anonymous function +Changing the behavior of a smart contract can be done by customizing the implementation through lambda's function. + +So the idea is to define an anonymous function in the storage which is called in entrypoint and write an entrypoint that allow to change implementation of this anonymous function. + +Let's consider the "starmap" smart contract : -Let's consider the "starmap" smart contract ``` // starmap.ligo type coordinates = { x : int, y : int, z : int } diff --git a/src/frontend/src/pages/Chapters/Reason/ChapterMultisig/course.md b/src/frontend/src/pages/Chapters/Reason/ChapterMultisig/course.md index 994432b..ef9d2ec 100644 --- a/src/frontend/src/pages/Chapters/Reason/ChapterMultisig/course.md +++ b/src/frontend/src/pages/Chapters/Reason/ChapterMultisig/course.md @@ -12,7 +12,7 @@ When invoking a smart contract, an entrypoint is called and usually an action is The purpose of a multi-signature pattern is to execute an action when all preconditions has been verified. The action that need to be executed depends on the smart contract logic. The mutli-signature implementation can be done in a single contract with the smart contract logic or in a separated contract like a proxy contract (which emits transactions to the contract containg the logic). -### rules +### Rules The multi-signature pattern can be described with this set of rules : From 271e2ac0c918a415022bda504941b3842a5ebfcd Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Fri, 19 Jun 2020 18:25:33 +0200 Subject: [PATCH 06/16] [Cameligo] chapter FA2 (part2) correction on NFT exemple + exercice --- .../Camel/ChapterFA20Permission/course.md | 22 ++++++++++------- .../Camel/ChapterFA20Permission/exercise.cmd | 8 ++++--- .../non_fungible_token.mligo | 24 ++++++++++--------- .../Camel/ChapterFA20Permission/solution.cmd | 5 ++++ 4 files changed, 37 insertions(+), 22 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution.cmd diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md index 8c96355..74e1abc 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md @@ -176,17 +176,23 @@ Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) ## Your mission We are working on a non_fungible/multi-asset token. -Our NFT "token" is almost ready but to allow a new rule. We need A to transfert a token taken from B account and send it to C account. +Our NFT "token" is almost ready but to allow a new rule. We need Bob to transfert a token taken from Vera account and send it to Alice account. 1- First we have to set the right operator policy to authorize delegation when deploying the contract. We want you to prepare the initial state of storage. Write the _ligo compile-storage_ command for the *token* contract with following recommandations : - * operator transfer is authorized, - * A account has a balance set to 100 - * B account has a balance set to 100 - * C account has a balance set to 100 - * D is the administrator of the contract + + * Jay 's account address is "tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" + * Alice's account address is "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" + * Bob's account address is "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU" + * Vera's account address is "tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv" + * operator transfer is authorized, + * Bob account has no token + * Vera account is owner of the token 1 + * Alice account has no token + * Jay is the administrator of the contract + * the token type transfered is 0 (token_id) -2- Write the _ligo dry-run_ command for authorizing A to transfer token taken from B account, transaction emitted by D. (reuse the storage you made on step 1) +2- Write the _ligo dry-run_ command for authorizing Bob to transfer token taken from Vera account, transaction emitted by Jay. (reuse the storage you made on step 1) -3- Write the _ligo dry-run_ command for simulating the transfer of 1 mutez from B'account to c's account, transaction emitted by A. You will have to modify the storage to take step 2 into account. +3- Write the _ligo dry-run_ command for simulating the transfer of 1 mutez from Vera'account to Alice's account, transaction emitted by A. You will have to modify the storage to take step 2 into account. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd index bb78a16..3d8e72b 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd @@ -1,3 +1,5 @@ -ligo compile-storage non_fungible_token.mligo main '' -ligo dry-run shiptoken.ligo 'Update_operators([{owner=TZ1;operator=TZ1}])' '' -ligo dry-run --sender=A shiptoken.ligo 'Transfer(B,C,token_id)' \ No newline at end of file +ligo compile-storage shiptoken.mligo main '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=(Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' + +ligo dry-run --sender=tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J shiptoken.mligo main 'Fa2 (Update_operators([ (Layout.convert_to_right_comb( Add_operator((Layout.convert_to_right_comb( ({owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address);operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)}: operator_param) ) : operator_param_michelson)) ) : update_operator_michelson) ]))' '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=(Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU shiptoken.mligo main 'Fa2 (Transfer( [ (Layout.convert_to_right_comb( ({from_=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); txs=[ (Layout.convert_to_right_comb( ({to_=("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address); token_id=0n; amount=1n }: transfer_destination)) : transfer_destination_michelson) ]} : transfer_aux)): transfer_michelson) ]))' '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=Set.add ({owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address);operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)}: operator_param) (Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo index e845d6a..1d18001 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo @@ -1,5 +1,5 @@ -#include "tzip/proposals/tzip-12/fa2_interface.mligo" -#include "tzip/proposals/tzip-12/fa2_errors.mligo" +#include "../tzip/proposals/tzip-12/fa2_interface.mligo" +#include "../tzip/proposals/tzip-12/fa2_errors.mligo" type entity = { code : string; @@ -10,7 +10,6 @@ type entity_key = (entity_id * token_id) type entities = (entity_key, entity) map type entityIndexToOwner = (entity_key, address) map type owner_entities = (address, entity_key set) map -//type owner_entities_count = (address, nat) map type token = { total_supply : nat; @@ -35,7 +34,9 @@ type entry_points = | Set_administrator of address | Mint of (entity * address * token_id) -type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or +type nft_entry_points = + | Fa2 of fa2_entry_points + | Nft of entry_points let set_pause(param,s : bool * storage): return = if Tezos.sender = s.administrator then @@ -51,9 +52,10 @@ let set_administrator(param,s : address * storage): return = let mint (param,s : (entity * address * token_id) * storage) : return = if Tezos.sender = s.administrator then - let new_ent : entity = param.0 in - let owner : address = param.1 in - let tokenid : token_id = param.2 in + // let new_ent : entity = param.0 in + // let owner : address = param.1 in + // let tokenid : token_id = param.2 in + let new_ent, owner, tokenid = param in // NEVER burn an entity or entity_id will be provided an existing id (mint burn mint) let newid : entity_id = match Map.find_opt tokenid s.tokens with | Some tok -> tok.total_supply + 1n @@ -237,9 +239,9 @@ let send_permissions_descriptor(param,s : (permissions_descriptor_michelson cont let op : operation = Tezos.transaction response 0mutez destination in ([ op ], s) -let main (param,s : total_entry_points * storage) : return = +let main (param,s : nft_entry_points * storage) : return = match param with - | M_left fa2_ep -> (match fa2_ep with + | Fa2 fa2_ep -> (match fa2_ep with | Transfer l -> transfer (l, s) | Balance_of p -> balance_of (p, s) | Total_supply p -> total_supply (p,s) @@ -248,8 +250,8 @@ let main (param,s : total_entry_points * storage) : return = | Update_operators l -> update_operators (l,s) | Is_operator o -> is_operator (o,s) ) - | M_right specific_ep -> (match specific_ep with + | Nft specific_ep -> (match specific_ep with | Set_pause p -> set_pause (p,s) | Set_administrator p -> set_administrator (p,s) | Mint ent -> mint (ent,s) - ) + ) \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution.cmd new file mode 100644 index 0000000..3d8e72b --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution.cmd @@ -0,0 +1,5 @@ +ligo compile-storage shiptoken.mligo main '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=(Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' + +ligo dry-run --sender=tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J shiptoken.mligo main 'Fa2 (Update_operators([ (Layout.convert_to_right_comb( Add_operator((Layout.convert_to_right_comb( ({owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address);operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)}: operator_param) ) : operator_param_michelson)) ) : update_operator_michelson) ]))' '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=(Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU shiptoken.mligo main 'Fa2 (Transfer( [ (Layout.convert_to_right_comb( ({from_=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); txs=[ (Layout.convert_to_right_comb( ({to_=("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address); token_id=0n; amount=1n }: transfer_destination)) : transfer_destination_michelson) ]} : transfer_aux)): transfer_michelson) ]))' '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=Set.add ({owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address);operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)}: operator_param) (Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' From b18fbeab8d4af34da22c5c57722da69de00e0cfc Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Tue, 23 Jun 2020 16:45:46 +0200 Subject: [PATCH 07/16] [Cameligo] chapter FA2 Hook --- .../Chapters/Camel/ChapterFA20Hook/course.md | 170 +++++++++------- .../Camel/ChapterFA20Hook/exercise.mligo | 75 +++++++ .../Camel/ChapterFA20Hook/fa2_core.mligo | 161 +++++++++++++++ .../Chapters/Camel/ChapterFA20Hook/index.ts | 30 +++ .../Camel/ChapterFA20Hook/solution.mligo | 84 ++++++++ .../ChapterFA20Hook/tzip-12/fa2_errors.mligo | 49 +++++ .../ChapterFA20Hook/tzip-12/fa2_hook.mligo | 32 +++ .../tzip-12/fa2_interface.mligo | 183 ++++++++++++++++++ .../tzip-12/lib/fa2_convertors.mligo | 154 +++++++++++++++ .../tzip-12/lib/fa2_operator_lib.mligo | 88 +++++++++ .../tzip-12/lib/fa2_owner_hooks_lib.mligo | 130 +++++++++++++ .../tzip-12/lib/fa2_transfer_hook_lib.mligo | 49 +++++ 12 files changed, 1131 insertions(+), 74 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/exercise.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/fa2_core.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/index.ts create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/solution.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_errors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_hook.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_interface.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_convertors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_owner_hooks_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.mligo diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md index 98df4ee..98b2ae5 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md @@ -55,9 +55,11 @@ In addition to the hook standard, the FA2 standard provides helper functions to #### FA2 standard hook library +##### Register FA2 core with Hook permission contract + Some helpers functions has been gatthered in a hook library which help defining hooks when implementing a FA2 contract. This library contains following functions and type alias : -The type _fa2_registry_ is a set of address +The type *fa2_registry* is a set of address. the function *get_hook_entrypoint* retrieves the contract interface of entrypoint "%tokens_transferred_hook" for a given contract address @@ -71,6 +73,40 @@ the function *create_register_hook_op* sends a transaction to a FA2 contract (ha the function *validate_hook_call* ensures an address in registered in the registry (set of address). + +##### Transfer Hooks + +The function *owners_transfer_hook* defined in the library generates a list of Tezos operations invoking sender and receiver hooks according to +the policies defined by the permissions descriptor. + + +The hook pattern depends on the permission policy. A transfer hook may be unwanted, optional or required. +If the policy requires a owner hook then the token owner contract MUST implement an entry point "tokens_received". Otherwise transfer is not allowed. +If the policy optionnaly accepts a owner hook then the token owner contract MAY implement an entry point "tokens_received". Otherwise transfer is allowed. + +It is the same for permission policies including senders, the entry point *tokens_sent* may need to be implemented. + +In case of a Transfer, if permission policies expect a hook, then the token owners MUST implement *fa2_token_receiver*, and *fa2_token_sender* interfaces. This implies that token'owner contract must have entry points *tokens_received* and *token_sent*. If these entry points fail the transfer is rejected. + + +##### Transfer Hooks entry points + +The library defines some helper functions + +The function *to_receiver_hook* retrieves the entry point *"%tokens_received"* for a given _address_. It enables to check if the *fa2_token_receiver* interface is implemented. + +The function *to_sender_hook* retrieves the entry point *"%tokens_sent"* for a given _address_. It enables to check if the *fa2_token_sender* interface is implemented. + +These two functions return a variant *hook_result* type. If variant value is *Hook_contract* then the entrypoint exists an is provided. If variant value is Hook_undefined then the entry point is not implemented and a message error is provided. + +``` +type hook_result = + | Hook_contract of transfer_descriptor_param_michelson contract + | Hook_undefined of string +``` + + + #### Hook Rules FA2 implementation with the transfer hook pattern recquires following rules: @@ -102,109 +138,95 @@ invoked hook fails, the whole transfer transaction MUST fail. FA2 token contract implements mint and burn operations, these operations MUST invoke a transfer hook as well. -#### Implementation of a custom hook +#### Implementation of a hook permission contract -Let's see an example of FA2 implementation. The following smart contract implements a token where transfer receiver must be in a whitelist. This whitelisting is done via a tranfer hook. -It uses a combination of a receiver white list and *fa2_token_receiver* interface. -Transfer is permitted if a receiver address is in the receiver white list OR implements *fa2_token_receiver* interface. +Let's see an example of FA2 Hook pattern implementation. The following smart contract implements a hook permission contract + +Owners transfer hooks are triggered by the *owners_transfer_hook* function. If a receiver address implements *fa2_token_receiver* interface, its *tokens_received* entry point must be called. +If a sender address implements *fa2_token_sender* interface, its *tokens_sent* entry point must be called. + ``` -#include "../lib/fa2_hook_lib.mligo" -#include "../lib/fa2_behaviors.mligo" +(** +Implementation of a generic permission transfer hook that supports sender/receiver +hooks. Contract behavior is driven by the permissions descriptor value in the +contract storage and its particular settings for `sender` and `receiver` policies. +*) +#include "../lib/fa2_transfer_hook_lib.mligo" +#include "../lib/fa2_owner_hooks_lib.mligo" type storage = { fa2_registry : fa2_registry; - receiver_whitelist : address set; -} - -let custom_validate_receivers (p, wl : transfer_descriptor_param * (address set)) - : operation list = - let get_receiver : get_owners = fun (tx : transfer_descriptor) -> - List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in - let receivers = get_owners_from_batch (p.batch, get_receiver) in - - Set.fold - (fun (ops, r : (operation list) * address) -> - let hook, err = to_sender_hook r in - match hook with - | Some h -> - let pm = transfer_descriptor_param_to_michelson p in - let op = Operation.transaction pm 0mutez h in - op :: ops - | None -> - if Set.mem r wl - then ops - else (failwith err : operation list) - ) - receivers ([] : operation list) - -let custom_transfer_hook (p, s : transfer_descriptor_param * storage) : operation list = - custom_validate_receivers (p, s.receiver_whitelist) - - -let get_policy_descriptor (u : unit) : permissions_descriptor = - { - operator = Owner_or_operator_transfer; - sender = Owner_no_hook; - receiver = Owner_no_hook ; (* overridden by the custom policy *) - custom = Some { - tag = "receiver_hook_and_whitelist"; - config_api = (Some Current.self_address); - }; - } - -type config_whitelist = - | Add_receiver_to_whitelist of address set - | Remove_receiver_from_whitelist of address set - -let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) - : address set = - match cfg with - | Add_receiver_to_whitelist rs -> - Set.fold - (fun (l, a : (address set) * address) -> Set.add a l) - rs wl - | Remove_receiver_from_whitelist rs -> - Set.fold - (fun (l, a : (address set) * address) -> Set.remove a l) - rs wl + descriptor : permissions_descriptor; +} type entry_points = - | Tokens_transferred_hook of transfer_descriptor_param + | Tokens_transferred_hook of transfer_descriptor_param_michelson | Register_with_fa2 of fa2_with_hook_entry_points contract - | Config_receiver_whitelist of config_whitelist let main (param, s : entry_points * storage) : (operation list) * storage = match param with - | Tokens_transferred_hook p -> - // verify s.fa2_registry contains p.fa2 address otherwise throw exception - let u = validate_hook_call (p.fa2, s.fa2_registry) in - let ops = custom_transfer_hook (p, s) in + | Tokens_transferred_hook pm -> + let p = transfer_descriptor_param_from_michelson pm in + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let ops = owners_transfer_hook + ({ligo_param = p; michelson_param = pm}, s.descriptor) in ops, s | Register_with_fa2 fa2 -> - let descriptor = get_policy_descriptor unit in - let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let op , new_registry = register_with_fa2 (fa2, s.descriptor, s.fa2_registry) in let new_s = { s with fa2_registry = new_registry; } in [op], new_s - | Config_receiver_whitelist cfg -> - let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in - let new_s = { s with receiver_whitelist = new_wl; } in - ([] : operation list), new_s + +(** example policies *) + +(* the policy which allows only token owners to transfer their own tokens. *) +let own_policy : permissions_descriptor = { + operator = Owner_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook; + custom = (None : custom_permission_policy option); +} ``` +Notice this Hook Permission contract contains an entry point *Register_with_fa2* to register with the FA2 core contract. + +Notice this Hook Permission contract contains an entry point *Tokens_transferred_hook* triggered when FA2 core contract receive a transfer request. This entry point triggers the owner hook transfer (sending hooks to sender and receiver and waiting for their approval or rejection). ## Your mission +We are working on a Fungible token which can handle multiple assets. We decided to implement a Hook pattern. A FA2 core contract handle all fa2 entry points (BalanceOf, Transfer, ...) and a hook permission contract which implements the validation of a transfer with some custom rules. + +1 - we want to accept a transfer if transfer receiver is registered in a whitelist. This whitelisting is done via a tranfer hook. + +2 - we want to accept a transfer if transfer receiver implements *fa2_token_receiver* interface. + +If a receiver address implements *fa2_token_receiver* interface, its *tokens_received* entry point must be called. + + +Complete the hook permission smart contract by implementing our custom rules on receivers. Transfer is permitted if receiver address implements *fa2_token_receiver* interface OR a receiver address is in the receiver white list. + +1- Find receiver hook - Check if a receiver _r_ implements *fa2_token_receiver* interface, using *to_receiver_hook* function and a _match_ operator. + +2- Retrieve hook - if the receiver _r_ implements *fa2_token_receiver* interface, introduce variable _h_ as hook entry point. + +3- Prepare parameter - cast parameter _p_ into type *transfer_descriptor_param_to_michelson* and store the result in a new variable _pm_ + +4- Call the entry point - create a variable _op_ of type *operation* which is a transaction sending variable _pm_ and no mutez to the retrieved entry point _h_ + +5- Return transactions - add this newly created operation _op_ in the returned list of operation _ops_ (and return _ops_) +6- if the receiver _r_ does not implement *fa2_token_receiver* interface, response of *to_receiver_hook* provided an error message with variable _err_. -1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. +7- Check if receiver _r_ is registered in the whitelist _wl_. +8- If it is the case , everything is fine, just return the returned list of operation _ops_. +9- Otherwise throw an exception with _err_ message. Don't forget to cast the exception. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/exercise.mligo new file mode 100644 index 0000000..581fffa --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/exercise.mligo @@ -0,0 +1,75 @@ +#include "tzip-12/lib/fa2_transfer_hook_lib.mligo" +#include "tzip-12/lib/fa2_owner_hooks_lib.mligo" + + +type storage = { + fa2_registry : fa2_registry; + receiver_whitelist : address set; +} + +let custom_validate_receivers (p, wl : transfer_descriptor_param * (address set)) + : operation list = + let get_receiver : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in + let receivers = get_owners_from_batch (p.batch, get_receiver) in + + Set.fold + (fun (ops, r : (operation list) * address) -> + // Type your solution below + ) + receivers ([] : operation list) + +let custom_transfer_hook (p, s : transfer_descriptor_param * storage) : operation list = + custom_validate_receivers (p, s.receiver_whitelist) + + +let get_policy_descriptor (u : unit) : permissions_descriptor = + { + operator = Owner_or_operator_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook ; (* overridden by the custom policy *) + custom = Some { + tag = "receiver_hook_and_whitelist"; + config_api = (Some Current.self_address); + }; + } + +type config_whitelist = + | Add_receiver_to_whitelist of address set + | Remove_receiver_from_whitelist of address set + +let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) + : address set = + match cfg with + | Add_receiver_to_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.add a l) + rs wl + | Remove_receiver_from_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.remove a l) + rs wl + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_receiver_whitelist of config_whitelist + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook p -> + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let ops = custom_transfer_hook (p, s) in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = get_policy_descriptor unit in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_receiver_whitelist cfg -> + let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in + let new_s = { s with receiver_whitelist = new_wl; } in + ([] : operation list), new_s diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/fa2_core.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/fa2_core.mligo new file mode 100644 index 0000000..a06e8ce --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/fa2_core.mligo @@ -0,0 +1,161 @@ +#if !FA2_MAC_TOKEN +#define FA2_MAC_TOKEN + +#include "tzip-12/fa2_interface.mligo" +#include "tzip-12/fa2_errors.mligo" +#include "tzip-12/lib/fa2_operator_lib.mligo" +#include "tzip-12/lib/fa2_owner_hooks_lib.mligo" + +(* (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 + +(* token_id -> token_metadata *) +type token_metadata_storage = (token_id, token_metadata_michelson) big_map + +type multi_token_storage = { + ledger : ledger; + operators : operator_storage; + token_total_supply : token_total_supply; + token_metadata : token_metadata_storage; + permissions_descriptor : permissions_descriptor; +} + +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 + 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 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 owner_validator function that validates of the tokens from the particular owner can be transferred. + *) +let transfer (txs, owner_validator, storage + : (transfer list) * ((address * operator_storage) -> unit) * multi_token_storage) + : ledger = + let make_transfer = fun (l, tx : ledger * transfer) -> + let u = owner_validator (tx.from_, storage.operators) in + 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 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 get_balance (p, ledger, tokens + : balance_of_param * ledger * token_total_supply) : 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_michelson) + else + let key = r.owner, r.token_id in + let bal = get_balance_amt (key, ledger) in + let response = { request = r; balance = bal; } in + balance_of_response_to_michelson response + in + let responses = List.map to_balance p.requests in + Operation.transaction responses 0mutez p.callback + +let get_owner_hook_ops_for_descriptor (tx_descriptor, pd + : transfer_descriptor_param * permissions_descriptor) : operation list = + let hook_calls = owners_transfer_hook (tx_descriptor, pd) in + match hook_calls with + | [] -> ([] : operation list) + | h :: t -> + let tx_descriptor_michelson = transfer_descriptor_param_to_michelson tx_descriptor in + List.map (fun(call: hook_entry_point) -> + Operation.transaction tx_descriptor_michelson 0mutez call) + hook_calls + +let get_owner_hook_ops (txs, pd : (transfer list) * permissions_descriptor) : operation list = + let tx_descriptors = transfers_to_descriptors txs in + let tx_descriptor : transfer_descriptor_param = { + operator = Tezos.sender; + batch = tx_descriptors; + } in + get_owner_hook_ops_for_descriptor (tx_descriptor, pd) + +let fa2_main (param, storage : fa2_entry_points * multi_token_storage) + : (operation list) * multi_token_storage = + match param with + | Transfer txs_michelson -> + (* convert transfer batch into `transfer_descriptor` batch *) + let txs = transfers_from_michelson txs_michelson in + (* + will validate that a sender is either `from_` parameter of each transfer + or a permitted operator for the owner `from_` address. + *) + let validator = make_default_operator_validator Tezos.sender in + let new_ledger = transfer (txs, validator, storage) in + let new_storage = { storage with ledger = new_ledger; } in + + let hook_ops = get_owner_hook_ops (txs, storage.permissions_descriptor) in + + (hook_ops), new_storage + + | Balance_of pm -> + let p = balance_of_param_from_michelson pm in + let op = get_balance (p, storage.ledger, storage.token_total_supply) in + [op], storage + + | Update_operators updates_michelson -> + let updates = operator_updates_from_michelson updates_michelson in + let updater = Tezos.sender in + let process_update = (fun (ops, update : operator_storage * update_operator) -> + let u = validate_update_operators_by_owner (update, updater) in + update_operators (update, ops) + ) in + let new_ops = + List.fold process_update updates storage.operators in + let new_storage = { storage with operators = new_ops; } in + ([] : operation list), new_storage + + | Token_metadata_registry callback -> + (* the contract maintains token metadata in its storage - `token_metadata` big_map *) + let callback_op = Operation.transaction Tezos.self_address 0mutez callback in + [callback_op], storage + +type fa2_multi_token_entry_points = + | FA2 of fa2_entry_points + | Permissions_descriptor of permissions_descriptor_michelson contract + +let get_permissions_descriptor (callback, storage + : permissions_descriptor_michelson contract * multi_token_storage) + : (operation list) * multi_token_storage = + let pdm = permissions_descriptor_to_michelson storage.permissions_descriptor in + let callback_op = Operation.transaction pdm 0mutez callback in + [callback_op], storage + +let fa2_multi_token_main (param, storage : fa2_multi_token_entry_points * multi_token_storage) + : (operation list) * multi_token_storage = + match param with + | FA2 fa2_param -> fa2_main (fa2_param, storage) + | Permissions_descriptor callback -> get_permissions_descriptor(callback, storage) +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/index.ts b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/index.ts new file mode 100644 index 0000000..3b1880a --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/index.ts @@ -0,0 +1,30 @@ +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import course from "!raw-loader!./course.md"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import exercise from "!raw-loader!./exercise.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import solution from "!raw-loader!./solution.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support1 from "!raw-loader!./tzip-12/lib/fa2_convertors.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support2 from "!raw-loader!./tzip-12/lib/fa2_operator_lib.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support3 from "!raw-loader!./tzip-12/lib/fa2_owner_hooks_lib.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support4 from "!raw-loader!./tzip-12/fa2_interface.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support5 from "!raw-loader!./tzip-12/fa2_errors.mligo"; + +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support6 from "!raw-loader!./fa2_core.mligo"; + +export const data = { course, exercise, solution, support1, support2, support3, support4, support5, support6 }; diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/solution.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/solution.mligo new file mode 100644 index 0000000..59aa417 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/solution.mligo @@ -0,0 +1,84 @@ +#include "tzip-12/lib/fa2_transfer_hook_lib.mligo" +#include "tzip-12/lib/fa2_owner_hooks_lib.mligo" + + +type storage = { + fa2_registry : fa2_registry; + receiver_whitelist : address set; +} + +let custom_validate_receivers (p, wl : transfer_descriptor_param * (address set)) + : operation list = + let get_receiver : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in + let receivers = get_owners_from_batch (p.batch, get_receiver) in + + Set.fold + (fun (ops, r : (operation list) * address) -> + // Type your solution below + match to_receiver_hook r with + | Hook_contract h -> + let pm = transfer_descriptor_param_to_michelson p in + let op = Operation.transaction pm 0mutez h in + op :: ops + | Hook_undefined err -> + if Set.mem r wl + then ops + else (failwith err : operation list) + ) + receivers ([] : operation list) + +let custom_transfer_hook (p, s : transfer_descriptor_param * storage) : operation list = + custom_validate_receivers (p, s.receiver_whitelist) + + +let get_policy_descriptor (u : unit) : permissions_descriptor = + { + operator = Owner_or_operator_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook ; (* overridden by the custom policy *) + custom = Some { + tag = "receiver_hook_and_whitelist"; + config_api = (Some Current.self_address); + }; + } + +type config_whitelist = + | Add_receiver_to_whitelist of address set + | Remove_receiver_from_whitelist of address set + +let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) + : address set = + match cfg with + | Add_receiver_to_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.add a l) + rs wl + | Remove_receiver_from_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.remove a l) + rs wl + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_receiver_whitelist of config_whitelist + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook p -> + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let ops = custom_transfer_hook (p, s) in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = get_policy_descriptor unit in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_receiver_whitelist cfg -> + let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in + let new_s = { s with receiver_whitelist = new_wl; } in + ([] : operation list), new_s diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_errors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_errors.mligo new file mode 100644 index 0000000..35a8ab8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_errors.mligo @@ -0,0 +1,49 @@ +#if !FA2_ERRORS +#define FA2_ERRORS + +(** One of the specified `token_id`s is not defined within the FA2 contract *) +let fa2_token_undefined = "FA2_TOKEN_UNDEFINED" +(** +A token owner does not have sufficient balance to transfer tokens from +owner's account +*) +let fa2_insufficient_balance = "FA2_INSUFFICIENT_BALANCE" +(** A transfer failed because of `operator_transfer_policy == No_transfer` *) +let fa2_tx_denied = "FA2_TX_DENIED" +(** +A transfer failed because `operator_transfer_policy == Owner_transfer` and it is +initiated not by the token owner +*) +let fa2_not_owner = "FA2_NOT_OWNER" +(** +A transfer failed because `operator_transfer_policy == Owner_or_operator_transfer` +and it is initiated neither by the token owner nor a permitted operator + *) +let fa2_not_operator = "FA2_NOT_OPERATOR" +(** +`update_operators` entry point is invoked and `operator_transfer_policy` is +`No_transfer` or `Owner_transfer` +*) +let fa2_operators_not_supported = "FA2_OPERATORS_UNSUPPORTED" +(** +Receiver hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let fa2_receiver_hook_failed = "FA2_RECEIVER_HOOK_FAILED" +(** +Sender hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let fa2_sender_hook_failed = "FA2_SENDER_HOOK_FAILED" +(** +Receiver hook is required by the permission behavior, but is not implemented by +a receiver contract + *) +let fa2_receiver_hook_undefined = "FA2_RECEIVER_HOOK_UNDEFINED" +(** +Sender hook is required by the permission behavior, but is not implemented by +a sender contract + *) +let fa2_sender_hook_undefined = "FA2_SENDER_HOOK_UNDEFINED" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_hook.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_hook.mligo new file mode 100644 index 0000000..f8dd01f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_hook.mligo @@ -0,0 +1,32 @@ +(** +Optional FA2 contract entry point to setup a transfer hook contract. +Transfer hook is one recommended design pattern to implement FA2 that enables +separation of the core token transfer logic and a permission policy. Instead of +implementing FA2 as a monolithic contract, a permission policy can be implemented +as a separate contract. Permission policy contract provides an entry point invoked +by the core FA2 contract to accept or reject a particular transfer operation (such +an entry point is called transfer hook) + *) + +#if !FA2_HOOK +#define FA2_HOOK + +#include "fa2_interface.mligo" + + +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb + +type fa2_with_hook_entry_points = + | Set_transfer_hook of set_hook_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_interface.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_interface.mligo new file mode 100644 index 0000000..5c9231a --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_interface.mligo @@ -0,0 +1,183 @@ +#if ! FA2_INTERFACE +#define FA2_INTERFACE + +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + +type transfer_michelson = transfer_aux michelson_pair_right_comb + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_request_michelson = balance_of_request michelson_pair_right_comb + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_response_aux = { + request : balance_of_request_michelson; + balance : nat; +} + +type balance_of_response_michelson = balance_of_response_aux michelson_pair_right_comb + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_aux = { + requests : balance_of_request_michelson list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_michelson = balance_of_param_aux michelson_pair_right_comb + +type operator_param = { + owner : address; + operator : address; +} + +type operator_param_michelson = operator_param michelson_pair_right_comb + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type update_operator_aux = + | Add_operator of operator_param_michelson + | Remove_operator of operator_param_michelson + +type update_operator_michelson = update_operator_aux michelson_or_right_comb + +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} + +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param = { + token_ids : token_id list; + handler : (token_metadata_michelson list) -> unit; +} + +type token_metadata_param_michelson = token_metadata_param michelson_pair_right_comb + +type fa2_entry_points = + | Transfer of transfer_michelson list + | Balance_of of balance_of_param_michelson + | Update_operators of update_operator_michelson list + | Token_metadata_registry of address contract + + +type fa2_token_metadata = + | Token_metadata of token_metadata_param_michelson + +(* permission policy definition *) + +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer + +type operator_transfer_policy_michelson = operator_transfer_policy michelson_or_right_comb + +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook + +type owner_hook_policy_michelson = owner_hook_policy michelson_or_right_comb + +type custom_permission_policy = { + tag : string; + config_api: address option; +} + +type custom_permission_policy_michelson = custom_permission_policy michelson_pair_right_comb + +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} + +type permissions_descriptor_aux = { + operator : operator_transfer_policy_michelson; + receiver : owner_hook_policy_michelson; + sender : owner_hook_policy_michelson; + custom : custom_permission_policy_michelson option; +} + +type permissions_descriptor_michelson = permissions_descriptor_aux michelson_pair_right_comb + +type fa2_entry_points_custom = + | Permissions_descriptor of permissions_descriptor_michelson contract + + +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param = { + batch : transfer_descriptor list; + operator : address; +} + +type transfer_descriptor_param_aux = { + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb + +type fa2_token_receiver = + | Tokens_received of transfer_descriptor_param_michelson + +type fa2_token_sender = + | Tokens_sent of transfer_descriptor_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_convertors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_convertors.mligo new file mode 100644 index 0000000..27b164b --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_convertors.mligo @@ -0,0 +1,154 @@ +(** +Helper function to convert FA2 entry points input parameters between their +Michelson and internal LIGO representation. + +FA2 contract implementation must conform to the Michelson entry points interface +outlined in the FA2 standard for interoperability with other contracts and off-chain +tools. + *) + +#if !FA2_CONVERTORS +#define FA2_CONVERTORS + +#include "../fa2_interface.mligo" + +let permissions_descriptor_to_michelson (d : permissions_descriptor) + : permissions_descriptor_michelson = + let aux : permissions_descriptor_aux = { + operator = Layout.convert_to_right_comb d.operator; + receiver = Layout.convert_to_right_comb d.receiver; + sender = Layout.convert_to_right_comb d.sender; + custom = match d.custom with + | None -> (None : custom_permission_policy_michelson option) + | Some c -> Some (Layout.convert_to_right_comb c) + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_to_michelson (p : transfer_descriptor) : transfer_descriptor_michelson = + let aux : transfer_descriptor_aux = { + from_ = p.from_; + txs = List.map + (fun (tx : transfer_destination_descriptor) -> + Layout.convert_to_right_comb tx + ) + p.txs; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_param_to_michelson (p : transfer_descriptor_param) + : transfer_descriptor_param_michelson = + let aux : transfer_descriptor_param_aux = { + operator = p.operator; + batch = List.map transfer_descriptor_to_michelson p.batch; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_from_michelson (p : transfer_descriptor_michelson) : transfer_descriptor = + let aux : transfer_descriptor_aux = Layout.convert_from_right_comb p in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_descriptor_michelson) -> + let tx : transfer_destination_descriptor = + Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfer_descriptor_param_from_michelson (p : transfer_descriptor_param_michelson) + : transfer_descriptor_param = + let aux : transfer_descriptor_param_aux = Layout.convert_from_right_comb p in + let b : transfer_descriptor list = + List.map transfer_descriptor_from_michelson aux.batch + in + { + operator = aux.operator; + batch = b; + } + +let transfer_from_michelson (txm : transfer_michelson) : transfer = + let aux : transfer_aux = Layout.convert_from_right_comb txm in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_michelson) -> + let tx : transfer_destination = Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfers_from_michelson (txsm : transfer_michelson list) : transfer list = + List.map transfer_from_michelson txsm + +let operator_param_from_michelson (p : operator_param_michelson) : operator_param = + let op : operator_param = Layout.convert_from_right_comb p in + op + +let operator_param_to_michelson (p : operator_param) : operator_param_michelson = + Layout.convert_to_right_comb p + +let operator_update_from_michelson (uom : update_operator_michelson) : update_operator = + let aux : update_operator_aux = Layout.convert_from_right_comb uom in + match aux with + | Add_operator opm -> Add_operator_p (operator_param_from_michelson opm) + | Remove_operator opm -> Remove_operator_p (operator_param_from_michelson opm) + +let operator_update_to_michelson (uo : update_operator) : update_operator_michelson = + let aux = match uo with + | Add_operator_p op -> Add_operator (operator_param_to_michelson op) + | Remove_operator_p op -> Remove_operator (operator_param_to_michelson op) + in + Layout.convert_to_right_comb aux + +let operator_updates_from_michelson (updates_michelson : update_operator_michelson list) + : update_operator list = + List.map operator_update_from_michelson updates_michelson + +let balance_of_param_from_michelson (p : balance_of_param_michelson) : balance_of_param = + let aux : balance_of_param_aux = Layout.convert_from_right_comb p in + let requests = List.map + (fun (rm : balance_of_request_michelson) -> + let r : balance_of_request = Layout.convert_from_right_comb rm in + r + ) + aux.requests + in + { + requests = requests; + callback = aux.callback; + } + +let balance_of_param_to_michelson (p : balance_of_param) : balance_of_param_michelson = + let aux : balance_of_param_aux = { + requests = List.map + (fun (r : balance_of_request) -> Layout.convert_to_right_comb r) + p.requests; + callback = p.callback; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_to_michelson (r : balance_of_response) : balance_of_response_michelson = + let aux : balance_of_response_aux = { + request = Layout.convert_to_right_comb r.request; + balance = r.balance; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_from_michelson (rm : balance_of_response_michelson) : balance_of_response = + let aux : balance_of_response_aux = Layout.convert_from_right_comb rm in + let request : balance_of_request = Layout.convert_from_right_comb aux.request in + { + request = request; + balance = aux.balance; + } + +let token_metas_to_michelson (ms : token_metadata list) : token_metadata_michelson list = + List.map + ( fun (m : token_metadata) -> + let mm : token_metadata_michelson = Layout.convert_to_right_comb m in + mm + ) ms + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.mligo new file mode 100644 index 0000000..eb15ab0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.mligo @@ -0,0 +1,88 @@ +(** +Reference implementation of the FA2 operator storage, config API and +helper functions +*) + +#if !FA2_OPERATOR_LIB +#define FA2_OPERATOR_LIB + +#include "fa2_convertors.mligo" +#include "../fa2_errors.mligo" + +(** +(owner, operator) -> unit +To be part of FA2 storage to manage permitted operators +*) +type operator_storage = ((address * address), unit) big_map + +(** + Updates operator storage using an `update_operator` command. + Helper function to implement `Update_operators` FA2 entry point +*) +let update_operators (update, storage : update_operator * operator_storage) + : operator_storage = + match update with + | Add_operator_p op -> + Big_map.update (op.owner, op.operator) (Some unit) storage + | Remove_operator_p op -> + Big_map.remove (op.owner, op.operator) storage + +(** +Validate if operator update is performed by the token owner. +@param updater an address that initiated the operation; usually `Tezos.sender`. +*) +let validate_update_operators_by_owner (update, updater : update_operator * address) + : unit = + let op = match update with + | Add_operator_p op -> op + | Remove_operator_p op -> op + in + if op.owner = updater then unit else failwith fa2_not_owner + +(** +Create an operator validator function based on provided operator policy. +@param tx_policy operator_transfer_policy defining the constrains on who can transfer. + *) +let make_operator_validator (tx_policy : operator_transfer_policy) + : (address * operator_storage)-> unit = + let can_owner_tx, can_operator_tx = match tx_policy with + | No_transfer -> (failwith fa2_tx_denied : bool * bool) + | Owner_transfer -> true, false + | Owner_or_operator_transfer -> true, true + in + let operator : address = Tezos.sender in + (fun (owner, ops_storage : address * operator_storage) -> + if can_owner_tx && owner = operator + then unit + else + if not can_operator_tx + then failwith fa2_not_owner + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith fa2_not_operator + ) + +(** +Default implementation of the operator validation function. +The default implicit `operator_transfer_policy` value is `Owner_or_operator_transfer` + *) +let make_default_operator_validator (operator : address) + : (address * operator_storage)-> unit = + (fun (owner, ops_storage : address * operator_storage) -> + if owner = operator + then unit + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith fa2_not_operator + ) + +(** +Validate operators for all transfers in the batch at once +@param tx_policy operator_transfer_policy defining the constrains on who can transfer. +*) +let validate_operator (tx_policy, txs, ops_storage + : operator_transfer_policy * (transfer list) * operator_storage) : unit = + let validator = make_operator_validator tx_policy in + List.iter (fun (tx : transfer) -> validator (tx.from_, ops_storage)) txs + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_owner_hooks_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_owner_hooks_lib.mligo new file mode 100644 index 0000000..942e019 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_owner_hooks_lib.mligo @@ -0,0 +1,130 @@ +#if !FA2_BEHAVIORS +#define FA2_BEHAVIORS + +(** +Generic implementation of the permission logic for sender and receiver hooks. +Actual behavior is driven by a `permissions_descriptor`. +To be used in FA2 and/or FA2 permission transfer hook contract implementation +which supports sender/receiver hooks. +*) + +#include "../fa2_interface.mligo" +#include "../fa2_errors.mligo" + +type get_owners = transfer_descriptor -> (address option) list + +type hook_result = + | Hook_contract of transfer_descriptor_param_michelson contract + | Hook_undefined of string + +type to_hook = address -> hook_result + +type transfer_hook_params = { + ligo_param : transfer_descriptor_param; + michelson_param : transfer_descriptor_param_michelson; +} + +(** +Extracts a set of unique `from_` or `to_` addresses from the transfer batch. +@param batch transfer batch +@param get_owner selector of `from_` or `to_` addresses from each individual `transfer_descriptor` + *) +let get_owners_from_batch (batch, get_owners : (transfer_descriptor list) * get_owners) : address set = + List.fold + (fun (acc, tx : (address set) * transfer_descriptor) -> + let owners = get_owners tx in + List.fold + (fun (acc, o: (address set) * (address option)) -> + match o with + | None -> acc + | Some a -> Set.add a acc + ) + owners + acc + ) + batch + (Set.empty : address set) + +let validate_owner_hook (p, get_owners, to_hook, is_required : + transfer_hook_params * get_owners * to_hook * bool) + : operation list = + let owners = get_owners_from_batch (p.ligo_param.batch, get_owners) in + Set.fold + (fun (ops, owner : (operation list) * address) -> + match to_hook owner with + | Hook_contract h -> + let op = Operation.transaction p.michelson_param 0mutez h in + op :: ops + | Hook_undefined error -> + (* owner hook is not implemented by the target contract *) + if is_required + then (failwith error : operation list) (* owner hook is required: fail *) + else ops (* owner hook is optional: skip it *) + ) + owners ([] : operation list) + +let validate_owner(p, policy, get_owners, to_hook : + transfer_hook_params * owner_hook_policy * get_owners * to_hook) + : operation list = + match policy with + | Owner_no_hook -> ([] : operation list) + | Optional_owner_hook -> validate_owner_hook (p, get_owners, to_hook, false) + | Required_owner_hook -> validate_owner_hook (p, get_owners, to_hook, true) + +(** +Given an address of the token receiver, tries to get an entry point for +`fa2_token_receiver` interface. + *) +let to_receiver_hook : to_hook = fun (a : address) -> + let c : (transfer_descriptor_param_michelson contract) option = + Operation.get_entrypoint_opt "%tokens_received" a in + match c with + | Some c -> Hook_contract c + | None -> Hook_undefined fa2_receiver_hook_undefined + +(** +Create a list iof Tezos operations invoking all token receiver contracts that +implement `fa2_token_receiver` interface. Fail if specified `owner_hook_policy` +cannot be met. + *) +let validate_receivers (p, receiver_policy : transfer_hook_params * owner_hook_policy) + : operation list = + let get_receivers : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_ )tx.txs in + validate_owner (p, receiver_policy, get_receivers, to_receiver_hook) + +(** +Given an address of the token sender, tries to get an entry point for +`fa2_token_sender` interface. + *) +let to_sender_hook : to_hook = fun (a : address) -> + let c : (transfer_descriptor_param_michelson contract) option = + Operation.get_entrypoint_opt "%tokens_sent" a in + match c with + | Some c -> Hook_contract c + | None -> Hook_undefined fa2_sender_hook_undefined + +(** +Create a list iof Tezos operations invoking all token sender contracts that +implement `fa2_token_sender` interface. Fail if specified `owner_hook_policy` +cannot be met. + *) +let validate_senders (p, sender_policy : transfer_hook_params * owner_hook_policy) + : operation list = + let get_sender : get_owners = fun (tx : transfer_descriptor) -> [tx.from_] in + validate_owner (p, sender_policy, get_sender, to_sender_hook) + +(** +Generate a list of Tezos operations invoking sender and receiver hooks according to +the policies defined by the permissions descriptor. +To be used in FA2 and/or FA2 transfer hook contract implementation which supports +sender/receiver hooks. + *) +let owners_transfer_hook (p, descriptor : transfer_hook_params * permissions_descriptor) + : operation list = + let sender_ops = validate_senders (p, descriptor.sender) in + let receiver_ops = validate_receivers (p, descriptor.receiver) in + (* merge two lists *) + List.fold (fun (l, o : (operation list) * operation) -> o :: l) receiver_ops sender_ops + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.mligo new file mode 100644 index 0000000..a4c99c1 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.mligo @@ -0,0 +1,49 @@ +(** + Helper types and functions to implement transfer hook contract. + Each transfer hook contract maintains a registry of known FA2 contracts and + validates that it is invoked from registered FA2 contracts. + + The implementation assumes that the transfer hook entry point is labeled as + `%tokens_transferred_hook`. + *) + +#if !FA2_HOOK_LIB +#define FA2_HOOK_LIB + +#include "../fa2_hook.mligo" +#include "fa2_convertors.mligo" + +let get_hook_entrypoint (hook_contract : address) (u : unit) + : transfer_descriptor_param_michelson contract = + let hook_entry : transfer_descriptor_param_michelson contract = + Operation.get_entrypoint "%tokens_transferred_hook" hook_contract in + hook_entry + + +let create_register_hook_op + (fa2, descriptor : (fa2_with_hook_entry_points contract) * permissions_descriptor) : operation = + let hook_fn = get_hook_entrypoint Current.self_address in + let p : set_hook_param_aux = { + hook = hook_fn; + permissions_descriptor = permissions_descriptor_to_michelson descriptor; + } in + let pm = Layout.convert_to_right_comb p in + Operation.transaction (Set_transfer_hook pm) 0mutez fa2 + + +type fa2_registry = address set + +let register_with_fa2 (fa2, descriptor, registry : + (fa2_with_hook_entry_points contract) * permissions_descriptor * fa2_registry) + : operation * fa2_registry = + let op = create_register_hook_op (fa2, descriptor) in + let fa2_address = Current.address fa2 in + let new_registry = Set.add fa2_address registry in + op, new_registry + +let validate_hook_call (fa2, registry: address * fa2_registry) : unit = + if Set.mem fa2 registry + then unit + else failwith "UNKNOWN_FA2_CALL" + +#endif From 666ab6d6cf2d84c447c38fb7785dae16bf3a0fa7 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Wed, 24 Jun 2020 13:57:52 +0200 Subject: [PATCH 08/16] [Cameligo] Rework/simplify chapter FA2 Operator --- .../src/pages/Chapter/Chapter.data.tsx | 21 + .../Chapters/Camel/ChapterFA20/exercise.mligo | 4 +- .../Chapters/Camel/ChapterFA20/solution.mligo | 4 +- .../Camel/ChapterFA20Operator/course.md | 201 +++ .../Camel/ChapterFA20Operator/exercise.cmd | 30 + .../Camel/ChapterFA20Operator/index.ts | 27 + .../Camel/ChapterFA20Operator/solution.cmd | 47 + .../Camel/ChapterFA20Operator/tqtz_nft.mligo | 160 +++ .../tzip-12/.gitattributes | 2 + .../examples/fa2_custom_receiver.mligo | 94 ++ .../tzip-12/examples/fa2_default_hook.mligo | 55 + .../examples/fa2_hook_with_schedule.mligo | 131 ++ .../tzip-12/examples/fa2_operator_lib.mligo | 159 +++ .../tzip-12/fa2_errors.mligo | 49 + .../tzip-12/fa2_hook.mligo | 32 + .../tzip-12/fa2_interface.mligo | 192 +++ .../tzip-12/implementing-fa2.md | 252 ++++ .../tzip-12/lib/fa2_convertors.mligo | 168 +++ .../tzip-12/lib/fa2_operator_lib.mligo | 88 ++ .../tzip-12/lib/fa2_owner_hooks_lib.mligo | 149 +++ .../tzip-12/lib/fa2_transfer_hook_lib.mligo | 49 + .../ChapterFA20Operator/tzip-12/tzip-12.md | 1093 +++++++++++++++++ .../solution_expanded.cmd | 137 +++ 23 files changed, 3140 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/course.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/exercise.cmd create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/index.ts create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/solution.cmd create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tqtz_nft.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/.gitattributes create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_custom_receiver.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_default_hook.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_hook_with_schedule.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_operator_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_errors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_hook.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_interface.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/implementing-fa2.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_convertors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_operator_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_owner_hooks_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_transfer_hook_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/tzip-12.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution_expanded.cmd diff --git a/src/frontend/src/pages/Chapter/Chapter.data.tsx b/src/frontend/src/pages/Chapter/Chapter.data.tsx index d751a28..aff006b 100644 --- a/src/frontend/src/pages/Chapter/Chapter.data.tsx +++ b/src/frontend/src/pages/Chapter/Chapter.data.tsx @@ -50,6 +50,9 @@ import { data as camelDataDeployContract } from "../Chapters/Camel/ChapterDeploy import { data as camelDataFA12 } from "../Chapters/Camel/ChapterFA12"; import { data as camelDataLambda } from "../Chapters/Camel/ChapterLambda"; import { data as camelDataMultisig } from "../Chapters/Camel/ChapterMultisig"; +import { data as camelFA20 } from "../Chapters/Camel/ChapterFA20"; +import { data as camelFA20Operator } from "../Chapters/Camel/ChapterFA20Operator"; +import { data as camelFA20Hook } from "../Chapters/Camel/ChapterFA20Hook"; import { data as reasonDataAddresses } from "../Chapters/Reason/ChapterAddresses"; import { data as reasonDataBuiltIns } from "../Chapters/Reason/ChapterBuiltIns"; @@ -283,6 +286,24 @@ export const chapterData = [ name: "25 - Camel - FA12", data: camelDataFA12, }, + { + pathname: "/camel/chapter-fa2", + language: "CameLIGO", + name: "26 - Camel - FA2", + data: camelFA20, + }, + { + pathname: "/camel/chapter-fa2-operator", + language: "CameLIGO", + name: "27 - Camel - FA2 Operator", + data: camelFA20Operator, + }, + { + pathname: "/camel/chapter-fa2-hook", + language: "CameLIGO", + name: "28 - Camel - FA2 Hook", + data: camelFA20Hook, + }, { pathname: "/reason/chapter-about", diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo index 9b61bd0..5f3484f 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo @@ -64,9 +64,7 @@ let total_supply(params, s: total_supply_param_michelson * storage) : return = let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in let token_ids : token_id list = p.token_ids in // Modify the code below - let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with - | Some v -> { token_id = i ; total_supply =v.total_supply } - | None -> (failwith(token_undefined) : total_supply_response) + let get_total_supply = in let responses : total_supply_response list = List.map get_total_supply token_ids in let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo index 5f3484f..9b61bd0 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo @@ -64,7 +64,9 @@ let total_supply(params, s: total_supply_param_michelson * storage) : return = let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in let token_ids : token_id list = p.token_ids in // Modify the code below - let get_total_supply = + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith(token_undefined) : total_supply_response) in let responses : total_supply_response list = List.map get_total_supply token_ids in let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/course.md new file mode 100644 index 0000000..062f58c --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/course.md @@ -0,0 +1,201 @@ +# Chapter 24 : Financial Asset 2.0 - Operators and Permissions + +Captain, why are you trying to change the part yourself? Just write a function on the terminal and send it to a droid. + +## Definition + +The FA2 standard proposes a *unified token contract interface* that accommodates all mentioned concerns. It aims to provide significant expressivity to contract developers to create new types of tokens while maintaining a common interface standard for wallet integrators and external developers. + +In this chapter we will focus on _Operators_ and _Permissions_. + +## Entry points + +Token contract implementing the FA2 standard MUST have the following entry points. + +``` +type fa2_entry_points = + +| Transfer of transfer list +| Balance_of of balance_of_param +| Total_supply of total_supply_param +| Token_metadata of token_metadata_param +| Permissions_descriptor of permissions_descriptor contract +| Update_operators of update_operator list +| Is_operator of is_operator_param +``` + +### Operators + +#### Definition +_Operator_ can be seen as delegate role. + +_Operator_ is a Tezos address that initiates token transfer operation on behalf of the owner. +_Owner_ is a Tezos address which can hold tokens. +An operator, other than the owner, MUST be approved to manage particular token types held by the owner to make a transfer from the owner account. +Operator relation is not transitive. If C is an operator of B , and if B is an operator of A, C cannot transfer tokens that are owned by A, on behalf of B. + +an _operator_ is defined as a relationship between two address (owner address and operator address) and can be understood as an operator address who can operate tokens held by a owner. + +#### FA2 interface operator + +FA2 interface specifies two entry points to update and inspect operators. Once permitted for the specific token owner, an operator can transfer any tokens belonging to the owner. + +``` +| Update_operators of update_operator_michelson list +| Is_operator of is_operator_param +``` + +where parameter type *update_operator* and *is_operator_param* are : +``` +type operator_param = { + owner : address; + operator : address; +} + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type is_operator_param = { + operator : operator_param; + callback : (is_operator_response_michelson) contract; +} +``` + +Notice the *update_operator* can only Add or Remove an _operator_ (an allowance between an operator address and a token owner address). + +Notice the parameter _is_operator_param_ given to *Is_operator* entry point contains a *callback* property used to send back a response to the calling contract. + +Notice entry point *Update_operators* expectes a list of *update_operator_michelson*. The fa2 convertor helper provide the *operator_param_to_michelson* function to convert *operator_param* format into *update_operator_michelson* format. + + +#### FA2 standard operator library + +Some helpers functions has been implemented in the FA2 library which help manipulating _operator_. This library contains following functions and type alias : + + +an _operator_ is a relationship between two address (owner address and operator address) + +function *is_operator* returns to a contract caller whether an operator address is associated to an owner address + +function *update_operators* allows to Add or Remove an operator in the list of operators. + +function *validate_update_operators_by_owner*, it ensures the given adress is owner of an _operator_ + +the function *validate_operator* validates operators for all transfers in the batch at once, depending on given operator_transfer_policy + + + +### FA2 Permission Policies and Configuration + +Most token standards specify logic that validates a transfer transaction and can either approve or reject a transfer. +Such logic (called _Permission Policy_) could validate who initiates a transfer, the transfer amount, and who can receive tokens. + +This FA2 standard defines a framework to compose and configure such permission policies from the standard behaviors and configuration APIs. + +FA2 defines : +* the default core transfer behavior, that MUST always be implemented +* a set of predefined permission policies that are optional + + +#### permissions_descriptor + +FA2 specifies an interface permissions_descriptor allowing external contracts to discover an FA2 contract's permission policy and to configure it. *permissions_descriptor* serves as a modular approach to define consistent and non-self-contradictory policies. + +The *permission descriptor* indicates which standard permission policies are implemented by the FA2 contract and can be used by off-chain and on-chain tools to discover the properties of the particular FA2 contract implementation. + +The FA2 standard defines a special metadata entry point *permission descriptor* containing standard permission policies. +``` +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} +``` + + +#### operator_transfer_policy + +operator_transfer_policy - defines who can transfer tokens. Tokens can be +transferred by the token owner or an operator (some address that is authorized to +transfer tokens on behalf of the token owner). A special case is when neither owner +nor operator can transfer tokens (can be used for non-transferable tokens). The +FA2 standard defines two entry points to manage and inspect operators associated +with the token owner address (*update_operators*, +*is_operator*). Once an operator is added, it can manage all of +its associated owner's tokens. + +``` +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer +``` + +#### owner_hook_policy + +owner_hook_policy - defines if sender/receiver hooks should be called or +not. Each token owner contract MAY implement either an *fa2_token_sender* or +*fa2_token_receiver* hook interface. Those hooks MAY be called when a transfer sends +tokens from the owner account or the owner receives tokens. The hook can either +accept a transfer transaction or reject it by failing. + +``` +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook +``` + +#### custom_permission_policy + +It is possible to extend permission policy with a custom behavior, which does +not overlap with already existing standard policies. This standard does not specify +exact types for custom config entry points. FA2 token contract clients that support +custom config entry points must know their types a priori and/or use a tag hint +of custom_permission_policy. + +``` +type custom_permission_policy = { + tag : string; + config_api: address option; +} +``` + + +#### Permission Policy Formulae + +Each concrete implementation of the permission policy can be described by a formula which combines permission behaviors in the following form: +``` +Operator(?) * Receiver(?) * Sender(?) +``` + +This formula describes the policy which allows only token owners to transfer their own +tokens : +``` +Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) +``` + + + + + +## Your mission + +We are working on a non_fungible/single-asset token. +Our NFT "token" is almost ready but to allow a new rule. We need Bob to transfert a token taken from Vera account and send it to Alice account. + + * Alice's account address is "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" + * Bob's account address is "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU" + * Vera's account address is "tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv" + +1- First we want you to prepare the initial state of storage. Modify the _ligo compile-storage_ command for the *token* contract with following recommandations : + + * Vera account is owner of the token 1 + +2- Complete the _ligo dry-run_ command for authorizing Bob to transfer token taken from Vera account, transaction emitted by Vera. (reuse the storage you made on step 1). You can use *operator_update_to_michelson* function to convert your parameters into the format expected by *Update_operators* entry point. + + +3- Complete the _ligo dry-run_ command for simulating the transfer of 1 token from Vera'account to Alice's account, transaction emitted by Bob. The transfered token id is number 1. You can use the *transfer_to_michelson* function to convert your parameters into the format expected by *Transfer* entry point. +You will have to modify the storage to in the state where "Vera account is owner of the token 1" (step 1) and Bob is authorized to transfer token taken from Vera account (step 2). diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/exercise.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/exercise.cmd new file mode 100644 index 0000000..5fa8aa6 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/exercise.cmd @@ -0,0 +1,30 @@ +ligo compile-storage tqtz_nft.mligo nft_token_main +'{ + ledger = (Big_map.empty : (token_id, address) big_map); + operators = (Big_map.empty : ((address * address), unit) big_map); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' + +ligo dry-run --sender=tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv tqtz_nft.mligo nft_token_main +'Fa2 (Update_operators([ + +]))' +'' + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU tqtz_nft.mligo nft_token_main +'Fa2 (Transfer( [ + +]))' +'{ + ledger = (Big_map.empty : (token_id, address) big_map); + operators = (Big_map.empty : ((address * address), unit) big_map); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/index.ts b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/index.ts new file mode 100644 index 0000000..77e1835 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/index.ts @@ -0,0 +1,27 @@ +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import course from "!raw-loader!./course.md"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import exercise from "!raw-loader!./exercise.cmd"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import solution from "!raw-loader!./solution.cmd"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support1 from "!raw-loader!./tzip-12/lib/fa2_convertors.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support2 from "!raw-loader!./tzip-12/lib/fa2_operator_lib.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support4 from "!raw-loader!./tzip-12/fa2_interface.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support5 from "!raw-loader!./tzip-12/fa2_errors.mligo"; + +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support6 from "!raw-loader!./tqtz_nft.mligo"; + +export const data = { course, exercise, solution, support1, support2, support4, support5, support6 }; diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/solution.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/solution.cmd new file mode 100644 index 0000000..3501d2f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/solution.cmd @@ -0,0 +1,47 @@ +ligo compile-storage tqtz_nft.mligo nft_token_main +'{ + ledger = Big_map.literal([(1n,("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]); + operators = (Big_map.empty : ((address * address), unit) big_map); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' + +ligo dry-run --sender=tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv tqtz_nft.mligo nft_token_main +'Fa2 (Update_operators([ + operator_update_to_michelson (Add_operator_p({ + owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address) + })) +]))' +'{ + ledger = Big_map.literal([(1n,("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]); + operators = (Big_map.empty : ((address * address), unit) big_map); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU tqtz_nft.mligo nft_token_main +'Fa2 (Transfer( [ + transfer_to_michelson ({ + from_=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + txs=[{ + to_=("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address); + token_id=1n; + amount=1n}] + }) +]))' +'{ + ledger = Big_map.literal([(1n,("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]); + operators = Big_map.literal([ ((("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv": address), ("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)), unit) ]); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tqtz_nft.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tqtz_nft.mligo new file mode 100644 index 0000000..f2e661f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tqtz_nft.mligo @@ -0,0 +1,160 @@ +(** +Implementation of the FA2 interface for the NFT contract supporting multiple +types of NFTs. Each NFT type is represented by the range of token IDs - `token_def`. + *) +#if !FA2_NFT_TOKEN +#define FA2_NFT_TOKEN + +#include "tzip-12/fa2_interface.mligo" +#include "tzip-12/fa2_errors.mligo" +#include "tzip-12/lib/fa2_operator_lib.mligo" + +(* range of nft tokens *) +type token_def = { + from_ : nat; + to_ : nat; +} + +type nft_meta = (token_def, token_metadata) big_map + +type token_storage = { + token_defs : token_def set; + last_used_id : token_id; + metadata : nft_meta; +} + +type ledger = (token_id, address) big_map + +type nft_token_storage = { + ledger : ledger; + operators : operator_storage; + metadata : token_storage; +} + +(** +Retrieve the balances for the specified tokens and owners +@return callback operation +*) +let get_balance (p, ledger : balance_of_param * ledger) : operation = + let to_balance = fun (r : balance_of_request) -> + let owner = Big_map.find_opt r.token_id ledger in + let response = match owner with + | None -> (failwith fa2_token_undefined : balance_of_response) + | Some o -> + let bal = if o = r.owner then 1n else 0n in + { request = r; balance = bal; } + in + balance_of_response_to_michelson response + in + let responses = List.map to_balance p.requests in + Operation.transaction responses 0mutez p.callback + +(** +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 owner_validator function that validates of the tokens from the particular owner can be transferred. + *) +let transfer (txs, owner_validator, ops_storage, ledger + : (transfer list) * ((address * operator_storage) -> unit) * operator_storage * ledger) : ledger = + (* process individual transfer *) + let make_transfer = (fun (l, tx : ledger * transfer) -> + let u = owner_validator (tx.from_, ops_storage) in + List.fold + (fun (ll, dst : ledger * transfer_destination) -> + if dst.amount = 0n + then ll + else if dst.amount <> 1n + then (failwith fa2_insufficient_balance : ledger) + else + let owner = Big_map.find_opt dst.token_id ll in + match owner with + | None -> (failwith fa2_token_undefined : ledger) + | Some o -> + if o <> tx.from_ + then (failwith fa2_insufficient_balance : ledger) + else Big_map.update dst.token_id (Some dst.to_) ll + ) tx.txs l + ) + in + + List.fold make_transfer txs ledger + +(** Finds a definition of the token type (token_id range) associated with the provided token id *) +let find_token_def (tid, token_defs : token_id * (token_def set)) : token_def = + let tdef = Set.fold (fun (res, d : (token_def option) * token_def) -> + match res with + | Some r -> res + | None -> + if tid >= d.from_ && tid < d.to_ + then Some d + else (None : token_def option) + ) token_defs (None : token_def option) + in + match tdef with + | None -> (failwith fa2_token_undefined : token_def) + | Some d -> d + +let get_metadata (tokens, meta : (token_id list) * token_storage ) + : token_metadata list = + List.map (fun (tid: token_id) -> + let tdef = find_token_def (tid, meta.token_defs) in + let meta = Big_map.find_opt tdef meta.metadata in + match meta with + | Some m -> { m with token_id = tid; } + | None -> (failwith "NO_DATA" : token_metadata) + ) tokens + +let fa2_main (param, storage : fa2_entry_points * nft_token_storage) + : (operation list) * nft_token_storage = + match param with + | Transfer txs_michelson -> + let txs = transfers_from_michelson txs_michelson in + let validator = make_default_operator_validator Tezos.sender in + let new_ledger = transfer (txs, validator, storage.operators, storage.ledger) in + let new_storage = { storage with ledger = new_ledger; } + in ([] : operation list), new_storage + + | Balance_of pm -> + let p = balance_of_param_from_michelson pm in + let op = get_balance (p, storage.ledger) in + [op], storage + + | Update_operators updates_michelson -> + let updates = operator_updates_from_michelson updates_michelson in + let updater = Tezos.sender in + let process_update = (fun (ops, update : operator_storage * update_operator) -> + let u = validate_update_operators_by_owner (update, updater) in + update_operators (update, ops) + ) in + let new_ops = + List.fold process_update updates storage.operators in + let new_storage = { storage with operators = new_ops; } in + ([] : operation list), new_storage + + | Token_metadata_registry callback -> + (* the contract stores its own token metadata and exposes `token_metadata` entry point *) + let callback_op = Operation.transaction Tezos.self_address 0mutez callback in + [callback_op], storage + + + + type nft_entry_points = + | Fa2 of fa2_entry_points + | Metadata of fa2_token_metadata + + let nft_token_main (param, storage : nft_entry_points * nft_token_storage) + : (operation list) * nft_token_storage = + match param with + | Fa2 fa2 -> fa2_main (fa2, storage) + | Metadata m -> ( match m with + | Token_metadata pm -> + let p : token_metadata_param = Layout.convert_from_right_comb pm in + let metas = get_metadata (p.token_ids, storage.metadata) in + let metas_michelson = token_metas_to_michelson metas in + let u = p.handler metas_michelson in + ([] : operation list), storage + ) + + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/.gitattributes b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/.gitattributes new file mode 100644 index 0000000..e2bd9c3 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/.gitattributes @@ -0,0 +1,2 @@ +*.mligo gitlab-language=ocaml +*.religo gitlab-language=ocaml \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_custom_receiver.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_custom_receiver.mligo new file mode 100644 index 0000000..e14dd61 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_custom_receiver.mligo @@ -0,0 +1,94 @@ +(** +Implementation of the permission transfer hook, with custom behavior. +It uses a combination of a receiver while list and `fa2_token_receiver` interface. +Transfer is permitted if a receiver address is in the receiver white list OR implements +`fa2_token_receiver` interface. If a receiver address implements `fa2_token_receiver` +interface, its `tokens_received` entry point must be called. +*) + +#include "../lib/fa2_transfer_hook_lib.mligo" +#include "../lib/fa2_owner_hooks_lib.mligo" + + +type storage = { + fa2_registry : fa2_registry; + receiver_whitelist : address set; +} + +let custom_validate_receivers (p, pm, wl + : transfer_descriptor_param * transfer_descriptor_param_michelson * (address set)) + : operation list = + let get_receiver : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in + let receivers = get_owners_from_batch (p.batch, get_receiver) in + Set.fold + (fun (ops, r : (operation list) * address) -> + match to_receiver_hook r with + | Hook_entry_point h -> + (* receiver contract implements fa2_token_receiver interface: invoke it*) + let op = Operation.transaction pm 0mutez h in + op :: ops + | Hook_undefined err -> + (* receiver contract does not implement fa2_token_receiver interface: check whitelist*) + if Set.mem r wl + then ops + else (failwith err : operation list) + ) + receivers ([] : operation list) + +let custom_transfer_hook (p, pm, s + : transfer_descriptor_param * transfer_descriptor_param_michelson * storage) : operation list = + custom_validate_receivers (p, pm, s.receiver_whitelist) + + +let get_policy_descriptor (u : unit) : permissions_descriptor = + { + operator = Owner_or_operator_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook ; (* overridden by the custom policy *) + custom = Some { + tag = "receiver_hook_and_whitelist"; + config_api = (Some Current.self_address); + }; + } + +type config_whitelist = + | Add_receiver_to_whitelist of address set + | Remove_receiver_from_whitelist of address set + +let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) + : address set = + match cfg with + | Add_receiver_to_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.add a l) + rs wl + | Remove_receiver_from_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.remove a l) + rs wl + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param_michelson + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_receiver_whitelist of config_whitelist + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook pm -> + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let p = transfer_descriptor_param_from_michelson pm in + let ops = custom_transfer_hook (p, pm, s) in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = get_policy_descriptor unit in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_receiver_whitelist cfg -> + let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in + let new_s = { s with receiver_whitelist = new_wl; } in + ([] : operation list), new_s diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_default_hook.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_default_hook.mligo new file mode 100644 index 0000000..ff006d8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_default_hook.mligo @@ -0,0 +1,55 @@ +(** +Implementation of a generic permission transfer hook that supports sender/receiver +hooks. Contract behavior is driven by the permissions descriptor value in the +contract storage and its particular settings for `sender` and `receiver` policies. +*) + +#include "../lib/fa2_transfer_hook_lib.mligo" +#include "../lib/fa2_owner_hooks_lib.mligo" + +type storage = { + fa2_registry : fa2_registry; + descriptor : permissions_descriptor; +} + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param_michelson + | Register_with_fa2 of fa2_with_hook_entry_points contract + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook pm -> + let p = transfer_descriptor_param_from_michelson pm in + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let hook_calls = owners_transfer_hook (p, s.descriptor) in + let ops = List.map (fun (call : hook_entry_point) -> + Operation.transaction pm 0mutez call + ) hook_calls + in + ops, s + + | Register_with_fa2 fa2 -> + let op , new_registry = register_with_fa2 (fa2, s.descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + + +(** example policies *) + +(* the policy which allows only token owners to transfer their own tokens. *) +let own_policy : permissions_descriptor = { + operator = Owner_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook; + custom = (None : custom_permission_policy option); +} + +(* non-transferable token (neither token owner, nor operators can transfer tokens. *) + let own_policy : permissions_descriptor = { + operator = No_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook; + custom = (None : custom_permission_policy option); +} diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_hook_with_schedule.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_hook_with_schedule.mligo new file mode 100644 index 0000000..5c7cc89 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_hook_with_schedule.mligo @@ -0,0 +1,131 @@ +(** +Implementation of a generic permission transfer hook that supports sender/receiver +hooks. Contract behavior is driven by the permissions descriptor value in the +contract storage and its particular settings for `sender` and `receiver` policies. + +It is possible to use additional custom policy "schedule" which let pause/unpause +transfers based on used schedule +*) + +#include "../lib/fa2_transfer_hook_lib.mligo" +#include "../lib/fa2_owner_hooks_lib.mligo" + +type schedule_interval = { + interval : int; + locked : bool; +} + +type schedule = { + start : timestamp; + schedule : schedule_interval list; + cyclic : bool; +} + +type schedule_policy = { + schedule : schedule; + schedule_interval : int; +} + +type permission_policy = { + descriptor : permissions_descriptor; + schedule_policy : schedule_policy option; +} + +type storage = { + fa2_registry : fa2_registry; + policy : permission_policy; +} + +type schedule_config = + | Set_schedule of schedule + | View_schedule of (schedule option) contract + +let configure_schedule (cfg, policy : schedule_config * schedule_policy option) + : (operation list) * (schedule_policy option) = + match cfg with + | Set_schedule s -> + let total_interval = List.fold + (fun (t, i : int * schedule_interval) -> t + i.interval) + s.schedule 0 in + let new_policy : schedule_policy = { schedule = s; schedule_interval = total_interval; } in + ([] : operation list), (Some new_policy) + | View_schedule v -> + let s = match policy with + | Some p -> Some p.schedule + | None -> (None : schedule option) + in + let op = Operation.transaction s 0mutez v in + [op], policy + +let custom_policy_to_descriptor (p : permission_policy) : permissions_descriptor = + match p.schedule_policy with + | None -> p.descriptor + | Some s -> + let custom_p : custom_permission_policy = { + tag = "schedule"; + config_api = Some Current.self_address; + } + in + {p.descriptor with custom = Some custom_p; } + +type interval_result = + | Reminder of int + | Found of schedule_interval + +let is_schedule_locked (policy : schedule_policy) : bool = + let elapsed : int = Current.time - policy.schedule.start in + if elapsed > policy.schedule_interval && not policy.schedule.cyclic + then true + else (* find schedule interval *) + let e = (elapsed mod policy.schedule_interval) + 0 in + let interval = List.fold + (fun (acc, i : interval_result * schedule_interval) -> + match acc with + | Found si -> acc + | Reminder r -> + if r < i.interval then Found i + else Reminder (r - i.interval) + ) policy.schedule.schedule (Reminder e) in + match interval with + | Reminder r -> (failwith "SCHEDULE_ERROR" : bool) + | Found i -> i.locked + +let validate_schedule (policy : schedule_policy option) : unit = + match policy with + | None -> unit + | Some p -> + let locked = is_schedule_locked p in + if locked + then failwith "SCHEDULE_LOCKED" + else unit + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param_michelson + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_schedule of schedule_config + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook pm -> + let p = transfer_descriptor_param_from_michelson pm in + let u1 = validate_hook_call (Tezos.sender, s.fa2_registry) in + let u2 = validate_schedule(s.policy.schedule_policy) in + let hook_calls = owners_transfer_hook(p, s.policy.descriptor) in + let ops = List.map (fun (call : hook_entry_point) -> + Operation.transaction pm 0mutez call + ) hook_calls + in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = custom_policy_to_descriptor s.policy in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_schedule cfg -> + let ops, new_schedule = configure_schedule (cfg, s.policy.schedule_policy) in + let new_s = { s with policy.schedule_policy = new_schedule; } in + ops, new_s + diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_operator_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_operator_lib.mligo new file mode 100644 index 0000000..d0fc331 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_operator_lib.mligo @@ -0,0 +1,159 @@ +(** Reference implementation of the FA2 operator storage and config API functions *) + +#include "../fa2_interface.mligo" + +type operator_tokens_entry = + | All_operator_tokens + | Some_operator_tokens of token_id set + | All_operator_tokens_except of token_id set + +(* (owner * operator) -> tokens *) +type operator_storage = ((address * address), operator_tokens_entry) big_map + +let add_tokens (existing_ts, ts_to_add : (operator_tokens_entry option) * (token_id set)) + : operator_tokens_entry = + match existing_ts with + | None -> Some_operator_tokens ts_to_add + | Some ets -> ( + match ets with + | All_operator_tokens -> All_operator_tokens + | Some_operator_tokens ets -> + (* merge sets *) + let new_ts = Set.fold + (fun (acc, tid : (token_id set) * token_id) -> Set.add tid acc) + ts_to_add ets in + Some_operator_tokens new_ts + | All_operator_tokens_except ets -> + (* subtract sets *) + let new_ts = Set.fold + (fun (acc, tid : (token_id set) * token_id) -> Set.remove tid acc) + ts_to_add ets in + if (Set.size new_ts) = 0n + then All_operator_tokens + else All_operator_tokens_except new_ts + ) + +let add_operator (op, storage : operator_param * operator_storage) : operator_storage = + let key = op.owner, op.operator in + let new_tokens = match op.tokens with + | All_tokens -> All_operator_tokens + | Some_tokens ts_to_add -> + let existing_tokens = Big_map.find_opt key storage in + add_tokens (existing_tokens, ts_to_add) + in + Big_map.update key (Some new_tokens) storage + +let remove_tokens (existing_ts, ts_to_remove : (operator_tokens_entry option) * (token_id set)) + : operator_tokens_entry option = + match existing_ts with + | None -> (None : operator_tokens_entry option) + | Some ets -> ( + match ets with + | All_operator_tokens -> Some (All_operator_tokens_except ts_to_remove) + | Some_operator_tokens ets -> + (* subtract sets *) + let new_ts = Set.fold + (fun (acc, tid : (token_id set) * token_id) -> Set.remove tid acc) + ts_to_remove ets in + if (Set.size new_ts) = 0n + then (None : operator_tokens_entry option) + else Some (Some_operator_tokens new_ts) + | All_operator_tokens_except ets -> + (* merge sets *) + let new_ts = Set.fold + (fun (acc, tid : (token_id set) * token_id) -> Set.add tid acc) + ts_to_remove ets in + Some (All_operator_tokens_except new_ts) + ) + +let remove_operator (op, storage : operator_param * operator_storage) : operator_storage = + let key = op.owner, op.operator in + let new_tokens_opt = match op.tokens with + | All_tokens -> (None : operator_tokens_entry option) + | Some_tokens ts_to_remove -> + let existing_tokens = Big_map.find_opt key storage in + remove_tokens (existing_tokens, ts_to_remove) + in + Big_map.update key new_tokens_opt storage + +let are_tokens_included (existing_tokens, ts : operator_tokens_entry * operator_tokens) : bool = + match existing_tokens with + | All_operator_tokens -> true + | Some_operator_tokens ets -> ( + match ts with + | All_tokens -> false + | Some_tokens ots -> + (* all ots tokens must be in ets set*) + Set.fold (fun (res, ti : bool * token_id) -> + if (Set.mem ti ets) then res else false + ) ots true + ) + | All_operator_tokens_except ets -> ( + match ts with + | All_tokens -> false + | Some_tokens ots -> + (* None of the its tokens must be in ets *) + Set.fold (fun (res, ti : bool * token_id) -> + if (Set.mem ti ets) then false else res + ) ots true + ) + +let is_operator_impl (p, storage : operator_param * operator_storage) : bool = + let key = p.owner, p.operator in + let op_tokens = Big_map.find_opt key storage in + match op_tokens with + | None -> false + | Some existing_tokens -> are_tokens_included (existing_tokens, p.tokens) + +let update_operators (params, storage : (update_operator list) * operator_storage) + : operator_storage = + List.fold + (fun (s, up : operator_storage * update_operator) -> + match up with + | Add_operator op -> add_operator (op, s) + | Remove_operator op -> remove_operator (op, s) + ) params storage + +let is_operator (param, storage : is_operator_param * operator_storage) : operation = + let is_op = is_operator_impl (param.operator, storage) in + let r : is_operator_response = { + operator = param.operator; + is_operator = is_op; + } in + Operation.transaction r 0mutez param.callback + +type owner_to_tokens = (address, (token_id set)) map + +let validate_operator (self, txs, ops_storage + : self_transfer_policy * (transfer list) * operator_storage) : unit = + let can_self_tx = match self with + | Self_transfer_permitted -> true + | Self_transfer_denied -> false + in + let operator = Current.sender in + let tokens_by_owner = List.fold + (fun (owners, tx : owner_to_tokens * transfer) -> + let tokens = Map.find_opt tx.from_ owners in + let new_tokens = match tokens with + | None -> Set.literal [tx.token_id] + | Some ts -> Set.add tx.token_id ts + in + Map.update tx.from_ (Some new_tokens) owners + ) txs (Map.empty : owner_to_tokens) in + + Map.iter + (fun (owner, tokens : address * (token_id set)) -> + if can_self_tx && owner = operator + then unit + else + let oparam : operator_param = { + owner = owner; + operator = sender; + tokens = Some_tokens tokens; + } in + let is_op = is_operator_impl (oparam, ops_storage) in + if is_op then unit else failwith "not permitted operator" + ) tokens_by_owner + + +let test(u : unit) = unit \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_errors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_errors.mligo new file mode 100644 index 0000000..35a8ab8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_errors.mligo @@ -0,0 +1,49 @@ +#if !FA2_ERRORS +#define FA2_ERRORS + +(** One of the specified `token_id`s is not defined within the FA2 contract *) +let fa2_token_undefined = "FA2_TOKEN_UNDEFINED" +(** +A token owner does not have sufficient balance to transfer tokens from +owner's account +*) +let fa2_insufficient_balance = "FA2_INSUFFICIENT_BALANCE" +(** A transfer failed because of `operator_transfer_policy == No_transfer` *) +let fa2_tx_denied = "FA2_TX_DENIED" +(** +A transfer failed because `operator_transfer_policy == Owner_transfer` and it is +initiated not by the token owner +*) +let fa2_not_owner = "FA2_NOT_OWNER" +(** +A transfer failed because `operator_transfer_policy == Owner_or_operator_transfer` +and it is initiated neither by the token owner nor a permitted operator + *) +let fa2_not_operator = "FA2_NOT_OPERATOR" +(** +`update_operators` entry point is invoked and `operator_transfer_policy` is +`No_transfer` or `Owner_transfer` +*) +let fa2_operators_not_supported = "FA2_OPERATORS_UNSUPPORTED" +(** +Receiver hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let fa2_receiver_hook_failed = "FA2_RECEIVER_HOOK_FAILED" +(** +Sender hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let fa2_sender_hook_failed = "FA2_SENDER_HOOK_FAILED" +(** +Receiver hook is required by the permission behavior, but is not implemented by +a receiver contract + *) +let fa2_receiver_hook_undefined = "FA2_RECEIVER_HOOK_UNDEFINED" +(** +Sender hook is required by the permission behavior, but is not implemented by +a sender contract + *) +let fa2_sender_hook_undefined = "FA2_SENDER_HOOK_UNDEFINED" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_hook.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_hook.mligo new file mode 100644 index 0000000..f8dd01f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_hook.mligo @@ -0,0 +1,32 @@ +(** +Optional FA2 contract entry point to setup a transfer hook contract. +Transfer hook is one recommended design pattern to implement FA2 that enables +separation of the core token transfer logic and a permission policy. Instead of +implementing FA2 as a monolithic contract, a permission policy can be implemented +as a separate contract. Permission policy contract provides an entry point invoked +by the core FA2 contract to accept or reject a particular transfer operation (such +an entry point is called transfer hook) + *) + +#if !FA2_HOOK +#define FA2_HOOK + +#include "fa2_interface.mligo" + + +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb + +type fa2_with_hook_entry_points = + | Set_transfer_hook of set_hook_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_interface.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_interface.mligo new file mode 100644 index 0000000..4245064 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_interface.mligo @@ -0,0 +1,192 @@ +#if ! FA2_INTERFACE +#define FA2_INTERFACE + +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + +type transfer_michelson = transfer_aux michelson_pair_right_comb + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_request_michelson = balance_of_request michelson_pair_right_comb + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_response_aux = { + request : balance_of_request_michelson; + balance : nat; +} + +type balance_of_response_michelson = balance_of_response_aux michelson_pair_right_comb + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_aux = { + requests : balance_of_request_michelson list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_michelson = balance_of_param_aux michelson_pair_right_comb + +type operator_param = { + owner : address; + operator : address; +} + +type operator_param_michelson = operator_param michelson_pair_right_comb + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type update_operator_aux = + | Add_operator of operator_param_michelson + | Remove_operator of operator_param_michelson + +type update_operator_michelson = update_operator_aux michelson_or_right_comb + +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} + +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param = { + token_ids : token_id list; + handler : (token_metadata_michelson list) -> unit; +} + +type token_metadata_param_michelson = token_metadata_param michelson_pair_right_comb + +type fa2_entry_points = + | Transfer of transfer_michelson list + | Balance_of of balance_of_param_michelson + | Update_operators of update_operator_michelson list + | Token_metadata_registry of address contract + + +type fa2_token_metadata = + | Token_metadata of token_metadata_param_michelson + +(* permission policy definition *) + +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer + +type operator_transfer_policy_michelson = operator_transfer_policy michelson_or_right_comb + +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook + +type owner_hook_policy_michelson = owner_hook_policy michelson_or_right_comb + +type custom_permission_policy = { + tag : string; + config_api: address option; +} + +type custom_permission_policy_michelson = custom_permission_policy michelson_pair_right_comb + +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} + +type permissions_descriptor_aux = { + operator : operator_transfer_policy_michelson; + receiver : owner_hook_policy_michelson; + sender : owner_hook_policy_michelson; + custom : custom_permission_policy_michelson option; +} + +type permissions_descriptor_michelson = permissions_descriptor_aux michelson_pair_right_comb + +(* permissions descriptor entry point +type fa2_entry_points_custom = + ... + | Permissions_descriptor of permissions_descriptor_michelson contract + +*) + + +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param = { + batch : transfer_descriptor list; + operator : address; +} + +type transfer_descriptor_param_aux = { + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb +(* +Entry points for sender/receiver hooks + +type fa2_token_receiver = + ... + | Tokens_received of transfer_descriptor_param_michelson + +type fa2_token_sender = + ... + | Tokens_sent of transfer_descriptor_param_michelson +*) + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/implementing-fa2.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/implementing-fa2.md new file mode 100644 index 0000000..9ae0fa0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/implementing-fa2.md @@ -0,0 +1,252 @@ + +# Implementing FA2 + +## Transfer Hook + +Transfer hook is one recommended design pattern to implement FA2 that enables +separation of the core token transfer logic and a permission policy. Instead of +implementing FA2 as a monolithic contract, a [permission policy] +(#fa2-permission-policies-and-configuration) can be implemented as a separate +contract. Permission policy contract provides an entry point invoked by the core +FA2 contract to accept or reject a particular transfer operation (such +an entry point is called **transfer hook**). + +### Transfer Hook Motivation + +Usually, different tokens require different permission policies that define who +can transfer and receive tokens. There is no single permission policy that fits +all scenarios. For instance, some game tokens can be transferred by token owners, +but no one else. In some financial token exchange applications, tokens are to be +transferred by a special exchange operator account, not directly by the token owners +themselves. + +Support for different permission policies usually requires customizing existing +contract code. The FA2 standard proposes a different approach in which the on-chain +composition of the core FA2 contract implementation does not change, and a pluggable +permission transfer hook is implemented as a separate contract and registered with +the core FA2. Every time FA2 performs a transfer, it invokes a hook contract that +validates a transaction and either approves it by finishing execution successfully +or rejects it by failing. + +The transfer hook makes it possible to model different transfer permission +policies like whitelists, operator lists, etc. Although this approach introduces +gas consumption overhead (compared to an all-in-one contract) by requiring an extra +inter-contract call, it also offers some other advantages: + +* FA2 core implementation can be verified once, and certain properties (not + related to permission policy) remain unchanged. + +* Most likely, the core transfer semantic will remain unchanged. If + modification of the permission policy is required for an existing contract, it + can be done by replacing a transfer hook only. No storage migration of the FA2 + ledger is required. + +* Transfer hooks could be used for purposes beyond permissioning, such as + implementing custom logic for a particular token application. + +### Transfer Hook Specification + +An FA2 token contract has a single entry point to set the hook. If a transfer hook +is not set, the FA2 token contract transfer operation MUST fail. Transfer hook is +to be set by the token contract administrator before any transfers can happen. +The concrete token contract implementation MAY impose additional restrictions on +who may set the hook. If the set hook operation is not permitted, it MUST fail +without changing existing hook configuration. + +For each transfer operation, a token contract MUST invoke a transfer hook and +return a corresponding operation as part of the transfer entry point result. +(For more details see [`set_transfer_hook`](#set_transfer_hook) ) + +`operator` parameter for the hook invocation MUST be set to `SENDER`. + +`from_` parameter for each `hook_transfer` batch entry MUST be set to `Some(transfer.from_)`. + +`to_` parameter for each `hook_transfer` batch entry MUST be set to `Some(transfer.to_)`. + +A transfer hook MUST be invoked, and operation returned by the hook invocation +MUST be returned by `transfer` entry point among other operations it might create. +`SENDER` MUST be passed as an `operator` parameter to any hook invocation. If an +invoked hook fails, the whole transfer transaction MUST fail. + +FA2 does NOT specify an interface for mint and burn operations; however, if an +FA2 token contract implements mint and burn operations, these operations MUST +invoke a transfer hook as well. + +| Mint | Burn | +| :---- | :--- | +| Invoked if registered. `from_` parameter MUST be `None` | Invoked if registered. `to_` parameter MUST be `None`| + +Note that using the transfer hook design pattern with sender/receiver hooks may +potentially be insecure. Sender and/or receiver contract hooks will be called +from the transfer hook contract (not the facade FA2 token contract). If sender/receiver +contracts rely on `SENDER` value for authorization, they must guarantee that the +call is initiated on behalf of the FA2 contract. + +### `set_transfer_hook` + +FA2 entry point with the following signature. + +LIGO definition: + +```ocaml +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +| Set_transfer_hook of set_hook_param_michelson +``` + +where + +```ocaml +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param_aux = { + fa2 : address; + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb +``` + +Michelson definition: + +``` +(pair %set_transfer_hook + (lambda %hook + unit + (contract + (pair + (address %fa2) + (pair + (list %batch + (pair + (option %from_ address) + (list %txs + (pair + (option %to_ address) + (pair + (nat %token_id) + (nat %amount) + ) + ) + ) + ) + ) + (address %operator) + ) + ) + ) + ) + (pair %permissions_descriptor + (or %operator + (unit %no_transfer) + (or + (unit %owner_transfer) + (unit %owner_or_operator_transfer) + ) + ) + (pair + (or %receiver + (unit %owner_no_op) + (or + (unit %optional_owner_hook) + (unit %required_owner_hook) + ) + ) + (pair + (or %sender + (unit %owner_no_op) + (or + (unit %optional_owner_hook) + (unit %required_owner_hook) + ) + ) + (option %custom + (pair + (string %tag) + (option %config_api address) + ) + ) + ) + ) + ) +) +``` + +FA2 implementation MAY restrict access to this operation to a contract administrator +address only. + +The parameter is an address plus hook entry point of type +`transfer_descriptor_param`. + +The transfer hook is always invoked from the `transfer` operation; otherwise, FA2 +MUST fail. + +`hook` field in `set_hook_param` record is a lambda which returns a hook entry +point of type `transfer_descriptor_param`. It allows a policy contract implementor +to choose a name for the hook entry point or even implement several transfer hooks +in the same contract. + +### Transfer Hook Examples + +#### Default Permission Policy + +Only a token owner can initiate a transfer of tokens from their accounts ( `from_` +MUST be equal to `SENDER`). + +Any address can be a recipient of the token transfer. + +[Hook contract](./examples/fa2_default_hook.mligo) + +#### Custom Receiver Hook/White List Permission Policy + +This is a sample implementation of the FA2 transfer hook, which supports receiver +whitelist and `fa2_token_receiver` for token receivers. The hook contract also +supports [operators](#operator-transfer-behavior). + +Only addresses that are whitelisted or implement the `fa2_token_receiver` interface +can receive tokens. If one or more `to_` addresses in FA2 transfer batch are not +permitted, the whole transfer operation MUST fail. + +The following table demonstrates the required actions depending on `to_` address +properties. + +| `to_` is whitelisted | `to_` implements `fa2_token_receiver` interface | Action | +| ------ | ----- | ----------| +| No | No | Transaction MUST fail | +| Yes | No | Continue transfer | +| No | Yes | Continue transfer, MUST call `tokens_received` | +| Yes | Yes | Continue transfer, MUST call `tokens_received` | + +Permission policy formula `S(true) * O(true) * ROH(None) * SOH(Custom)`. + +[Hook contract](./examples/fa2_custom_receiver.mligo) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_convertors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_convertors.mligo new file mode 100644 index 0000000..e31a506 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_convertors.mligo @@ -0,0 +1,168 @@ +(** +Helper function to convert FA2 entry points input parameters between their +Michelson and internal LIGO representation. + +FA2 contract implementation must conform to the Michelson entry points interface +outlined in the FA2 standard for interoperability with other contracts and off-chain +tools. + *) + +#if !FA2_CONVERTORS +#define FA2_CONVERTORS + +#include "../fa2_interface.mligo" + +let permissions_descriptor_to_michelson (d : permissions_descriptor) + : permissions_descriptor_michelson = + let aux : permissions_descriptor_aux = { + operator = Layout.convert_to_right_comb d.operator; + receiver = Layout.convert_to_right_comb d.receiver; + sender = Layout.convert_to_right_comb d.sender; + custom = match d.custom with + | None -> (None : custom_permission_policy_michelson option) + | Some c -> Some (Layout.convert_to_right_comb c) + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_to_michelson (p : transfer_descriptor) : transfer_descriptor_michelson = + let aux : transfer_descriptor_aux = { + from_ = p.from_; + txs = List.map + (fun (tx : transfer_destination_descriptor) -> + Layout.convert_to_right_comb tx + ) + p.txs; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_param_to_michelson (p : transfer_descriptor_param) + : transfer_descriptor_param_michelson = + let aux : transfer_descriptor_param_aux = { + operator = p.operator; + batch = List.map transfer_descriptor_to_michelson p.batch; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_from_michelson (p : transfer_descriptor_michelson) : transfer_descriptor = + let aux : transfer_descriptor_aux = Layout.convert_from_right_comb p in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_descriptor_michelson) -> + let tx : transfer_destination_descriptor = + Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfer_descriptor_param_from_michelson (p : transfer_descriptor_param_michelson) + : transfer_descriptor_param = + let aux : transfer_descriptor_param_aux = Layout.convert_from_right_comb p in + let b : transfer_descriptor list = + List.map transfer_descriptor_from_michelson aux.batch + in + { + operator = aux.operator; + batch = b; + } + +let transfer_from_michelson (txm : transfer_michelson) : transfer = + let aux : transfer_aux = Layout.convert_from_right_comb txm in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_michelson) -> + let tx : transfer_destination = Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfers_from_michelson (txsm : transfer_michelson list) : transfer list = + List.map transfer_from_michelson txsm + +let transfer_to_michelson (tx : transfer) : transfer_michelson = + let aux : transfer_aux = { + from_ = tx.from_; + txs = List.map + (fun (tx: transfer_destination) -> + let t : transfer_destination_michelson = Layout.convert_to_right_comb tx in + t + ) tx.txs; + } in + Layout.convert_to_right_comb aux + +let transfers_to_michelson (txs : transfer list) : transfer_michelson list = + List.map transfer_to_michelson txs + +let operator_param_from_michelson (p : operator_param_michelson) : operator_param = + let op : operator_param = Layout.convert_from_right_comb p in + op + +let operator_param_to_michelson (p : operator_param) : operator_param_michelson = + Layout.convert_to_right_comb p + +let operator_update_from_michelson (uom : update_operator_michelson) : update_operator = + let aux : update_operator_aux = Layout.convert_from_right_comb uom in + match aux with + | Add_operator opm -> Add_operator_p (operator_param_from_michelson opm) + | Remove_operator opm -> Remove_operator_p (operator_param_from_michelson opm) + +let operator_update_to_michelson (uo : update_operator) : update_operator_michelson = + let aux = match uo with + | Add_operator_p op -> Add_operator (operator_param_to_michelson op) + | Remove_operator_p op -> Remove_operator (operator_param_to_michelson op) + in + Layout.convert_to_right_comb aux + +let operator_updates_from_michelson (updates_michelson : update_operator_michelson list) + : update_operator list = + List.map operator_update_from_michelson updates_michelson + +let balance_of_param_from_michelson (p : balance_of_param_michelson) : balance_of_param = + let aux : balance_of_param_aux = Layout.convert_from_right_comb p in + let requests = List.map + (fun (rm : balance_of_request_michelson) -> + let r : balance_of_request = Layout.convert_from_right_comb rm in + r + ) + aux.requests + in + { + requests = requests; + callback = aux.callback; + } + +let balance_of_param_to_michelson (p : balance_of_param) : balance_of_param_michelson = + let aux : balance_of_param_aux = { + requests = List.map + (fun (r : balance_of_request) -> Layout.convert_to_right_comb r) + p.requests; + callback = p.callback; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_to_michelson (r : balance_of_response) : balance_of_response_michelson = + let aux : balance_of_response_aux = { + request = Layout.convert_to_right_comb r.request; + balance = r.balance; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_from_michelson (rm : balance_of_response_michelson) : balance_of_response = + let aux : balance_of_response_aux = Layout.convert_from_right_comb rm in + let request : balance_of_request = Layout.convert_from_right_comb aux.request in + { + request = request; + balance = aux.balance; + } + +let token_metas_to_michelson (ms : token_metadata list) : token_metadata_michelson list = + List.map + ( fun (m : token_metadata) -> + let mm : token_metadata_michelson = Layout.convert_to_right_comb m in + mm + ) ms + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_operator_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_operator_lib.mligo new file mode 100644 index 0000000..eb15ab0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_operator_lib.mligo @@ -0,0 +1,88 @@ +(** +Reference implementation of the FA2 operator storage, config API and +helper functions +*) + +#if !FA2_OPERATOR_LIB +#define FA2_OPERATOR_LIB + +#include "fa2_convertors.mligo" +#include "../fa2_errors.mligo" + +(** +(owner, operator) -> unit +To be part of FA2 storage to manage permitted operators +*) +type operator_storage = ((address * address), unit) big_map + +(** + Updates operator storage using an `update_operator` command. + Helper function to implement `Update_operators` FA2 entry point +*) +let update_operators (update, storage : update_operator * operator_storage) + : operator_storage = + match update with + | Add_operator_p op -> + Big_map.update (op.owner, op.operator) (Some unit) storage + | Remove_operator_p op -> + Big_map.remove (op.owner, op.operator) storage + +(** +Validate if operator update is performed by the token owner. +@param updater an address that initiated the operation; usually `Tezos.sender`. +*) +let validate_update_operators_by_owner (update, updater : update_operator * address) + : unit = + let op = match update with + | Add_operator_p op -> op + | Remove_operator_p op -> op + in + if op.owner = updater then unit else failwith fa2_not_owner + +(** +Create an operator validator function based on provided operator policy. +@param tx_policy operator_transfer_policy defining the constrains on who can transfer. + *) +let make_operator_validator (tx_policy : operator_transfer_policy) + : (address * operator_storage)-> unit = + let can_owner_tx, can_operator_tx = match tx_policy with + | No_transfer -> (failwith fa2_tx_denied : bool * bool) + | Owner_transfer -> true, false + | Owner_or_operator_transfer -> true, true + in + let operator : address = Tezos.sender in + (fun (owner, ops_storage : address * operator_storage) -> + if can_owner_tx && owner = operator + then unit + else + if not can_operator_tx + then failwith fa2_not_owner + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith fa2_not_operator + ) + +(** +Default implementation of the operator validation function. +The default implicit `operator_transfer_policy` value is `Owner_or_operator_transfer` + *) +let make_default_operator_validator (operator : address) + : (address * operator_storage)-> unit = + (fun (owner, ops_storage : address * operator_storage) -> + if owner = operator + then unit + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith fa2_not_operator + ) + +(** +Validate operators for all transfers in the batch at once +@param tx_policy operator_transfer_policy defining the constrains on who can transfer. +*) +let validate_operator (tx_policy, txs, ops_storage + : operator_transfer_policy * (transfer list) * operator_storage) : unit = + let validator = make_operator_validator tx_policy in + List.iter (fun (tx : transfer) -> validator (tx.from_, ops_storage)) txs + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_owner_hooks_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_owner_hooks_lib.mligo new file mode 100644 index 0000000..35a9af1 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_owner_hooks_lib.mligo @@ -0,0 +1,149 @@ +#if !FA2_BEHAVIORS +#define FA2_BEHAVIORS + +(** +Generic implementation of the permission logic for sender and receiver hooks. +Actual behavior is driven by a `permissions_descriptor`. +To be used in FA2 and/or FA2 permission transfer hook contract implementation +which supports sender/receiver hooks. +*) + +#include "../fa2_interface.mligo" +#include "../fa2_errors.mligo" + +type get_owners = transfer_descriptor -> (address option) list + +type hook_entry_point = transfer_descriptor_param_michelson contract + +type hook_result = + | Hook_entry_point of hook_entry_point + | Hook_undefined of string + +type to_hook = address -> hook_result + +(* type transfer_hook_params = { + ligo_param : transfer_descriptor_param; + michelson_param : transfer_descriptor_param_michelson; +} *) + +(** +Extracts a set of unique `from_` or `to_` addresses from the transfer batch. +@param batch transfer batch +@param get_owner selector of `from_` or `to_` addresses from each individual `transfer_descriptor` + *) +let get_owners_from_batch (batch, get_owners : (transfer_descriptor list) * get_owners) : address set = + List.fold + (fun (acc, tx : (address set) * transfer_descriptor) -> + let owners = get_owners tx in + List.fold + (fun (acc, o: (address set) * (address option)) -> + match o with + | None -> acc + | Some a -> Set.add a acc + ) + owners + acc + ) + batch + (Set.empty : address set) + +let validate_owner_hook (p, get_owners, to_hook, is_required : + transfer_descriptor_param * get_owners * to_hook * bool) + : hook_entry_point list = + let owners = get_owners_from_batch (p.batch, get_owners) in + Set.fold + (fun (eps, owner : (hook_entry_point list) * address) -> + match to_hook owner with + | Hook_entry_point h -> h :: eps + | Hook_undefined error -> + (* owner hook is not implemented by the target contract *) + if is_required + then (failwith error : hook_entry_point list) (* owner hook is required: fail *) + else eps (* owner hook is optional: skip it *) + ) + owners ([] : hook_entry_point list) + +let validate_owner(p, policy, get_owners, to_hook : + transfer_descriptor_param * owner_hook_policy * get_owners * to_hook) + : hook_entry_point list = + match policy with + | Owner_no_hook -> ([] : hook_entry_point list) + | Optional_owner_hook -> validate_owner_hook (p, get_owners, to_hook, false) + | Required_owner_hook -> validate_owner_hook (p, get_owners, to_hook, true) + +(** +Given an address of the token receiver, tries to get an entry point for +`fa2_token_receiver` interface. + *) +let to_receiver_hook : to_hook = fun (a : address) -> + let c : hook_entry_point option = + Operation.get_entrypoint_opt "%tokens_received" a in + match c with + | Some c -> Hook_entry_point c + | None -> Hook_undefined fa2_receiver_hook_undefined + +(** +Create a list iof Tezos operations invoking all token receiver contracts that +implement `fa2_token_receiver` interface. Fail if specified `owner_hook_policy` +cannot be met. + *) +let validate_receivers (p, receiver_policy : transfer_descriptor_param * owner_hook_policy) + : hook_entry_point list = + let get_receivers : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_ )tx.txs in + validate_owner (p, receiver_policy, get_receivers, to_receiver_hook) + +(** +Given an address of the token sender, tries to get an entry point for +`fa2_token_sender` interface. + *) +let to_sender_hook : to_hook = fun (a : address) -> + let c : hook_entry_point option = + Operation.get_entrypoint_opt "%tokens_sent" a in + match c with + | Some c -> Hook_entry_point c + | None -> Hook_undefined fa2_sender_hook_undefined + +(** +Create a list iof Tezos operations invoking all token sender contracts that +implement `fa2_token_sender` interface. Fail if specified `owner_hook_policy` +cannot be met. + *) +let validate_senders (p, sender_policy : transfer_descriptor_param * owner_hook_policy) + : hook_entry_point list = + let get_sender : get_owners = fun (tx : transfer_descriptor) -> [tx.from_] in + validate_owner (p, sender_policy, get_sender, to_sender_hook) + +(** +Generate a list of Tezos operations invoking sender and receiver hooks according to +the policies defined by the permissions descriptor. +To be used in FA2 and/or FA2 transfer hook contract implementation which supports +sender/receiver hooks. + *) +let owners_transfer_hook (p, descriptor : transfer_descriptor_param * permissions_descriptor) + : hook_entry_point list = + let sender_entries = validate_senders (p, descriptor.sender) in + let receiver_entries = validate_receivers (p, descriptor.receiver) in + (* merge two lists *) + List.fold + (fun (l, ep : (hook_entry_point list) * hook_entry_point) -> ep :: l) + receiver_entries sender_entries + +let transfers_to_descriptors (txs : transfer list) : transfer_descriptor list = + List.map + (fun (tx : transfer) -> + let txs = List.map + (fun (dst : transfer_destination) -> + { + to_ = Some dst.to_; + token_id = dst.token_id; + amount = dst.amount; + } + ) tx.txs in + { + from_ = Some tx.from_; + txs = txs; + } + ) txs + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_transfer_hook_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_transfer_hook_lib.mligo new file mode 100644 index 0000000..a4c99c1 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_transfer_hook_lib.mligo @@ -0,0 +1,49 @@ +(** + Helper types and functions to implement transfer hook contract. + Each transfer hook contract maintains a registry of known FA2 contracts and + validates that it is invoked from registered FA2 contracts. + + The implementation assumes that the transfer hook entry point is labeled as + `%tokens_transferred_hook`. + *) + +#if !FA2_HOOK_LIB +#define FA2_HOOK_LIB + +#include "../fa2_hook.mligo" +#include "fa2_convertors.mligo" + +let get_hook_entrypoint (hook_contract : address) (u : unit) + : transfer_descriptor_param_michelson contract = + let hook_entry : transfer_descriptor_param_michelson contract = + Operation.get_entrypoint "%tokens_transferred_hook" hook_contract in + hook_entry + + +let create_register_hook_op + (fa2, descriptor : (fa2_with_hook_entry_points contract) * permissions_descriptor) : operation = + let hook_fn = get_hook_entrypoint Current.self_address in + let p : set_hook_param_aux = { + hook = hook_fn; + permissions_descriptor = permissions_descriptor_to_michelson descriptor; + } in + let pm = Layout.convert_to_right_comb p in + Operation.transaction (Set_transfer_hook pm) 0mutez fa2 + + +type fa2_registry = address set + +let register_with_fa2 (fa2, descriptor, registry : + (fa2_with_hook_entry_points contract) * permissions_descriptor * fa2_registry) + : operation * fa2_registry = + let op = create_register_hook_op (fa2, descriptor) in + let fa2_address = Current.address fa2 in + let new_registry = Set.add fa2_address registry in + op, new_registry + +let validate_hook_call (fa2, registry: address * fa2_registry) : unit = + if Set.mem fa2 registry + then unit + else failwith "UNKNOWN_FA2_CALL" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/tzip-12.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/tzip-12.md new file mode 100644 index 0000000..cf1f849 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/tzip-12.md @@ -0,0 +1,1093 @@ +--- +tzip: 12 +title: FA2 - Multi-Asset Interface +status: Draft +type: Financial Application (FA) +author: Eugene Mishura (@e-mishura) +created: 2020-01-24 +--- + +## Table Of Contents + +* [Summary](#summary) +* [Motivation](#motivation) +* [Abstract](#abstract) +* [Interface Specification](#interface-specification) + * [Entry Point Semantics](#entry-point-semantics) + * [`transfer`](#transfer) + * [Core `transfer` Behavior](#core-transfer-behavior) + * [Default `transfer` Permission Policy](#default-transfer-permission-policy) + * [`balance_of`](#balance_of) + * [Operators](#operators) + * [`update_operators`](#update_operators) + * [Token Metadata](#token-metadata) + * [`token_metadata_registry`](#token_metadata_registry) + * [`token_metadata` `big_map`](#token_metadata-big_map) + * [`token_metadata` Entry Point](#token_metadata-entry-point) + * [FA2 Permission Policies and Configuration](#fa2-permission-policies-and-configuration) + * [A Taxonomy of Permission Policies](#a-taxonomy-of-permission-policies) + * [`permissions_descriptor`](#permissions_descriptor) + * [Error Handling](#error-handling) +* [Implementing Different Token Types with FA2](#implementing-different-token-types-with-fa2) + * [Single Fungible Token](#single-fungible-token) + * [Multiple Fungible Tokens](#multiple-fungible-tokens) + * [Non-fungible Tokens](#non-fungible-tokens) + * [Mixing Fungible and Non-fungible Tokens](#mixing-fungible-and-non-fungible-tokens) + * [Non-transferable Tokens](#non-transferable-tokens) +* [Future Directions](#future-directions) +* [Copyright](#copyright) + +## Summary + +TZIP-12 proposes a standard for a unified token contract interface, +supporting a wide range of token types and implementations. This document provides +an overview and rationale for the interface, token transfer semantics, and support +for various transfer permission policies. + +**PLEASE NOTE:** This API specification remains a work-in-progress and may evolve +based on public comment see FA2 Request for Comment on [Tezos Agora](https://tezosagora.org). + +## Motivation + +There are multiple dimensions and considerations while implementing a particular +token smart contract. Tokens might be fungible or non-fungible. A variety of +permission policies can be used to define how many tokens can be transferred, who +can perform a transfer, and who can receive tokens. A token contract can be +designed to support a single token type (e.g. ERC-20 or ERC-721) or multiple token +types (e.g. ERC-1155) to optimize batch transfers and atomic swaps of the tokens. + +Such considerations can easily lead to the proliferation of many token standards, +each optimized for a particular token type or use case. This situation is apparent +in the Ethereum ecosystem, where many standards have been proposed, but ERC-20 +(fungible tokens) and ERC-721 (non-fungible tokens) are dominant. + +Token wallets, token exchanges, and other clients then need to support multiple +standards and multiple token APIs. The FA2 standard proposes a unified token +contract interface that accommodates all mentioned concerns. It aims to provide +significant expressivity to contract developers to create new types of tokens +while maintaining a common interface standard for wallet integrators and external +developers. + +## Abstract + +Token type is uniquely identified on the chain by a pair composed of the token +contract address and token ID, a natural number (`nat`). If the underlying contract +implementation supports only a single token type (e.g. ERC-20-like contract), +the token ID MUST be `0n`. In the case, when multiple token types are supported +within the same FA2 token contract (e. g. ERC-1155-like contract), the contract +is fully responsible for assigning and managing token IDs. FA2 clients MUST NOT +depend on particular ID values to infer information about a token. + +Most of the entry points are batch operations that allow querying or transfer of +multiple token types atomically. If the underlying contract implementation supports +only a single token type, the batch may contain single or multiple entries where +token ID will be a fixed `0n` value. Likewise, if multiple token types are supported, +the batch may contain zero or more entries and there may be duplicate token IDs. + +Most token standards specify logic that validates a transfer transaction and can +either approve or reject a transfer. Such logic could validate who can perform a +transfer, the transfer amount, and who can receive tokens. This standard calls such +logic a *permission policy*. The FA2 standard defines the +[default `transfer` permission policy](#default-transfer-permission-policy) that +specify who can transfer tokens. The default policy allows transfers by +either token owner (an account that holds token balance) or by an operator +(an account that is permitted to manage tokens on behalf of the token owner). + +Unlike many other standards, FA2 allows to customize the default permission policy +(see [FA2 Permission Policies and Configuration](#fa2-permission-policies-and-configuration)) +using a set of predefined permission policies that are optional. + +This specification defines the set of [standard errors](#error-handling) and error +mnemonics to be used when implementing FA2. However, some implementations MAY +introduce their custom errors that MUST follow the same pattern as standard ones. + +## Interface Specification + +Token contract implementing the FA2 standard MUST have the following entry points. +Notation is given in [cameLIGO language](https://ligolang.org) for readability +and Michelson. The LIGO definition, when compiled, generates compatible Michelson +entry points. + +`type fa2_entry_points =` + +* [`| Transfer of transfer list`](#transfer) +* [`| Balance_of of balance_of_param`](#balance_of) +* [`| Update_operators of update_operator list`](#update_operators) +* [`| Token_metadata_registry of address contract`](##token_metadata_registry) + +The full definition of the FA2 entry points in LIGO and related types can be found +in [fa2_interface.mligo](./fa2_interface.mligo). + +### Entry Point Semantics + +#### `transfer` + +LIGO definition: + +```ocaml +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +| Transfer of transfer_michelson list +``` + +
+where + +```ocaml +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + +type transfer_michelson = transfer_aux michelson_pair_right_comb +``` + +
+ +Michelson definition: +``` +(list %transfer + (pair + (address %from_) + (list %txs + (pair + (address %to_) + (pair + (nat %token_id) + (nat %amount) + ) + ) + ) + ) +) +``` + +Each transfer in the batch is specified between one source (`from_`) address and +a list of destinations. Each `transfer_destination` specifies token type and the +amount to be transferred from the source address to the destination (`to_`) address. + +FA2 does NOT specify an interface for mint and burn operations; however, if an +FA2 token contract implements mint and burn operations, it SHOULD, when possible, +enforce the same rules and logic applied to the token transfer operation. Mint +and burn can be considered special cases of the transfer. Although, it is possible +that mint and burn have more or less restrictive rules than the regular transfer. +For instance, mint and burn operations may be invoked by a special privileged +administrative address only. In this case, regular operator restrictions may not +be applicable. + +##### Core `transfer` Behavior + +FA2 token contracts MUST always implement this behavior. + +* Every transfer operation MUST happen atomically and in order. If at least one + transfer in the batch cannot be completed, the whole transaction MUST fail, all + token transfers MUST be reverted, and token balances MUST remain unchanged. + +* Each individual transfer MUST decrement token balance of the source (`from_`) + address by the amount of the transfer and increment token balance of the destination + (`to_`) address by the amount of the transfer. + +* If the transfer amount exceeds current token balance of the source address, + the whole transfer operation MUST fail with the error mnemonic `"FA2_INSUFFICIENT_BALANCE"`. + +* If the token owner does not hold any tokens of type `token_id`, the owner's balance + is interpreted as zero. No token owner can have a negative balance. + +* The transfer MUST update token balances exactly as the operation + parameters specify it. Transfer operations MUST NOT try to adjust transfer + amounts or try to add/remove additional transfers like transaction fees. + +* Transfers of zero amount MUST be treated as normal transfers. + +* If one of the specified `token_id`s is not defined within the FA2 contract, the + entry point MUST fail with the error mnemonic `"FA2_TOKEN_UNDEFINED"`. + +* Transfer implementations MUST apply permission policy logic (either + [default transfer permission policy](#default-transfer-permission-policy) or + [customized one](#customizing-permission-policy)). + If permission logic rejects a transfer, the whole operation MUST fail. + +* Core transfer behavior MAY be extended. If additional constraints on tokens + transfer are required, FA2 token contract implementation MAY invoke additional + permission policies. If the additional permission fails, the whole transfer + operation MUST fail with a custom error mnemonic. + +##### Default `transfer` Permission Policy + +* Token owner address MUST be able to perform a transfer of its own tokens (e. g. + `SENDER` equals to `from_` parameter in the `transfer`). + +* An operator (a Tezos address that performs token transfer operation on behalf + of the owner) MUST be permitted to manage all owner's tokens before it invokes + a transfer transaction (see [`update_operators`](#update_operators)). + +* If the address that invokes a transfer operation is neither a token owner nor + one of the permitted operators, the transaction MUST fail with the error mnemonic + `"FA2_NOT_OPERATOR"`. If at least one of the `transfer`s in the batch is not permitted, + the whole transaction MUST fail. + +#### `balance_of` + +LIGO definition: + +```ocaml +type token_id = nat + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} + +| Balance_of of balance_of_param_michelson +``` + +
+where + +```ocaml +type balance_of_request_michelson = balance_of_request michelson_pair_right_comb + +type balance_of_response_aux = { + request : balance_of_request_michelson; + balance : nat; +} + +type balance_of_response_michelson = balance_of_response_aux michelson_pair_right_comb + +type balance_of_param_aux = { + requests : balance_of_request_michelson list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_michelson = balance_of_param_aux michelson_pair_right_comb +``` +
+ +Michelson definition: + +``` +(pair %balance_of + (list %requests + (pair + (address %owner) + (nat %token_id) + ) + ) + (contract %callback + (list + (pair + (pair %request + (address %owner) + (nat %token_id) + ) + (nat %balance) + ) + ) + ) +) +``` + +Get the balance of multiple account/token pairs. Accepts a list of +`balance_of_request`s and a callback contract `callback` which accepts a list of +`balance_of_response` records. + +* There may be duplicate `balance_of_request`'s, in which case they should not be + deduplicated nor reordered. + +* If the account does not hold any tokens, the account + balance is interpreted as zero. + +* If one of the specified `token_id`s is not defined within the FA2 contract, the + entry point MUST fail with the error mnemonic `"FA2_TOKEN_UNDEFINED"`. + +*Notice:* The `balance_of` entry point implements a *continuation-passing style (CPS) +view entry point* pattern that invokes the other callback contract with the requested +data. This pattern, when not used carefully, could expose the callback contract +to an inconsistent state and/or manipulatable outcome (see +[view patterns](https://www.notion.so/Review-of-TZIP-12-95e4b631555d49429e2efdfe0f9ffdc0#6d68e18802734f059adf3f5ba8f32a74)). +The `balance_of` entry point should be used on the chain with the extreme caution. + +#### Operators + +**Operator** is a Tezos address that originates token transfer operation on behalf +of the owner. + +**Owner** is a Tezos address which can hold tokens. + +An operator, other than the owner, MUST be approved to manage all tokens held by +the owner to make a transfer from the owner account. + +FA2 interface specifies two entry points to update and inspect operators. Once +permitted for the specific token owner, an operator can transfer any tokens belonging +to the owner. + +##### `update_operators` + +LIGO definition: + +```ocaml +type operator_param = { + owner : address; + operator : address; +} + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +| Update_operators of update_operator_michelson list +``` + +
+where + +```ocaml +type operator_param_michelson = operator_param michelson_pair_right_comb + +type update_operator_aux = + | Add_operator of operator_param_michelson + | Remove_operator of operator_param_michelson + +type update_operator_michelson = update_operator_aux michelson_or_right_comb +``` + +
+ +Michelson definition: + +``` +(list %update_operators + (or + (pair %add_operator + (address %owner) + (address %operator) + ) + (pair %remove_operator + (address %owner) + (address %operator) + ) + ) +) +``` + +Add or Remove token operators for the specified owners. + +* The entry point accepts a list of `update_operator` commands. If two different + commands in the list add and remove an operator for the same owner, + the last command in the list MUST take effect. + +* It is possible to update operators for a token owner that does not hold any token + balances yet. + +* Operator relation is not transitive. If C is an operator of B , and if B is an + operator of A, C cannot transfer tokens that are owned by A, on behalf of B. + +The standard does not specify who is permitted to update operators on behalf of +the token owner. Depending on the business use case, the particular implementation +of the FA2 contract MAY limit operator updates to a token owner (`owner == SENDER`) +or be limited to an administrator. + +#### Token Metadata + +Each FA2 `token_id` has associated metadata of the following type: + +```ocaml +type token_id = nat + +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} +``` + +Michelson definition: + +``` +(pair + (nat %token_id) + (pair + (string %symbol) + (pair + (string %name) + (pair + (nat %decimals) + (map %extras string string) +)))) +``` + +* FA2 token amounts are represented by natural numbers (`nat`), and their + **granularity** (the smallest amount of tokens which may be minted, burned, or + transferred) is always 1. + +* `decimals` is the number of digits to use after the decimal point when displaying + the token amounts. If 0, the asset is not divisible. Decimals are used for display + purposes only and MUST NOT affect transfer operation. + +Examples + +| Decimals | Amount | Display | +| -------- | ------- | -------- | +| 0n | 123 | 123 | +| 1n | 123 | 12.3 | +| 3n | 123000 | 123 | + +Token metadata is primarily useful in off-chain, user-facing contexts (e.g. +wallets, explorers, marketplaces). As a result, FA2 optimizes for off-chain use +of token metadata and minimal on-chain gas consumption. A related effort to create +a separate metadata TZIP standard is also underway. + +* The FA2 contract MUST implement `token_metadata_registry` view entry point that + returns an address of the contract holding tokens metadata. Token metadata can + be held either by the FA2 token contract itself (then `token_metadata_registry` + returns `SELF` address) or by a separate token registry contract. + +* Token registry contract MUST implement one of two ways to expose token metadata + for off-chain clients: + + * Contract storage MUST have a `big_map` that maps `token_id -> token_metadata` + and annotated `%token_metadata` + + * Contract MUST implement entry point `token_metadata` + +##### `token_metadata_registry` + +LIGO definition: + +```ocaml +| Token_metadata_registry of address contract +``` + +Michelson definition: + +``` +(contract %token_metadata_registry address) +``` + +Return address of the contract that holds tokens metadata. If the FA2 contract +holds its own tokens metadata, the entry point returns `SELF` address. The entry +point parameter is some contract entry point to be called with the address of the +token metadata registry. + +##### `token_metadata` `big_map` + +LIGO definition: + +```ocaml +type = { + ... + token_metadata : (token_id, token_metadata) big_map; + ... +} +``` + +Michelson definition: + +``` +(big_map %token_metadata + nat + (pair + (nat %token_id) + (pair + (string %symbol) + (pair + (string %name) + (pair + (nat %decimals) + (map %extras string string) + )))) +) +``` + +The FA2 contract storage MUST have a `big_map` with a key type `token_id` and +value type `token_metadata`. This `big_map` MUST be annotated as `%token_metadata` +and can be at any position within the storage. + +##### `token_metadata` Entry Point + +LIGO definition: + +```ocaml +type token_metadata_param = { + token_ids : token_id list; + handler : (token_metadata_michelson list) -> unit; +} + +| Token_metadata of token_metadata_param_michelson +``` + +
+where + +```ocaml +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param_michelson = token_metadata_param michelson_pair_right_comb +``` + +Michelson definition: + +``` +(pair %token_metadata + (list %token_ids nat) + (lambda %handler + (list + (pair + (nat %token_id) + (pair + (string %symbol) + (pair + (string %name) + (pair + (nat %decimals) + (map %extras string string) + )))) + ) + unit + ) +) +``` + +
+ +Get the metadata for multiple token types. Accepts a list of `token_id`s and a +a lambda `handler`, which accepts a list of `token_metadata` records. The `handler` +lambda may assert certain assumptions about the metadata and/or fail with the +obtained metadata implementing a view entry point pattern to extract tokens metadata +off-chain. + +* As with `balance_of`, the input `token_id`'s should not be deduplicated nor + reordered. + +* If one of the specified `token_id`s is not defined within the FA2 contract, the + entry point MUST fail with the error mnemonic `"FA2_TOKEN_UNDEFINED"`. + +### FA2 Permission Policies and Configuration + +Most token standards specify logic such as who can perform a transfer, the amount +of a transfer, and who can receive tokens. This standard calls such logic *permission +policy* and defines a framework to compose such permission policies from the +[standard behaviors](#permission-behaviors). + +The FA2 contract developer can choose and implement a custom set of permissions +behaviors. The particular implementation may be static (the permissions configuration +cannot be changed after the contract is deployed) or dynamic (the FA2 contract +may be upgradable and allow to change the permissions configuration). At any moment +in time, the FA2 token contract MUST expose consistent and non-self-contradictory +permissions configuration (unlike ERC-777 that exposes two flavors of the transfer +at the same time). + +#### A Taxonomy of Permission Policies + +##### Permission Behaviors + +Permission policy semantics are composed from several orthogonal behaviors. +The concrete policy is expressed as a combination of those behaviors. Each permission +policy defines a set of possible standard behaviors. An FA2 contract developer MAY +chose to implement one or more behaviors that are different from the default ones +depending on their business use case. + +The FA2 defines the following standard permission behaviors, that can be chosen +independently, when an FA2 contract is implemented: + +###### `Operator` Permission Behavior + +This behavior specifies who is permitted to transfer tokens. + +Potentially token transfers can be performed by the token owner or by an operator +permitted to transfer tokens on behalf of the token owner. An operator can transfer +any tokens in any amount on behalf of the owner. + +```ocaml +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer (* default *) +``` + +* `No_transfer` - neither owner nor operator can transfer tokens. This permission + configuration can be used for non-transferable tokens or for the FA2 implementation + when a transfer can be performed only by some privileged and/or administrative + account. The transfer operation MUST fail with the error mnemonic `"FA2_TX_DENIED"`. + +* `Owner_transfer` - If `SENDER` is not the token owner, the transfer operation + MUST fail with the error mnemonic `"FA2_NOT_OWNER"`. + +* `Owner_or_operator_transfer` - allows transfer for the token owner or an operator + permitted to manage tokens on behalf of the owner. If `SENDER` is not the token + owner and not an operator permitted to manage tokens on behalf of the owner, + the transfer operation MUST fail with the error mnemonic `"FA2_NOT_OPERATOR"`. + The FA2 standard defines the entry point to manage operators associated with + the token owner address ([`update_operators`](#update_operators)). Once an + operator is added, it can manage all of its associated owner's tokens. + +* If an operator transfer is denied (`No_transfer` or `Owner_transfer`), +[`update_operators`](#update_operators) entry point MUST fail if invoked with the +error mnemonic `"FA2_OPERATORS_UNSUPPORTED"`. + +###### `Token Owner Hook` Permission Behavior + +Each transfer operation accepts a batch that defines token owners that send tokens +(senders) and token owners that receive tokens (receivers). Token owner contracts +MAY implement either `fa2_token_sender` and/or `fa2_token_receiver` interfaces. +Those interfaces define a hook entry point that accepts transfer description and +invoked by the FA2 contract in the context of transfer, mint and burn operations. + +Token owner permission can be configured to behave in one of the following ways: + +```ocaml +type owner_hook_policy = + | Owner_no_hook (* default *) + | Optional_owner_hook + | Required_owner_hook +``` + +* `Owner_no_hook` - ignore the owner hook interface. + +* `Optional_owner_hook` - treat the owner hook interface as optional. If a token +owner contract implements a corresponding hook interface, it MUST be invoked. If +the hook interface is not implemented, it gets ignored. + +* `Required_owner_hook` - treat the owner hook interface as required. If a token +owner contract implements a corresponding hook interface, it MUST be invoked. If +the hook interface is not implemented, the entire transfer transaction MUST fail. + +* Sender and/or receiver hooks can approve the transaction or reject it + by failing. If such a hook is invoked and failed, the whole transfer operation + MUST fail. + +* This policy can be applied to both token senders and token receivers. There are + two owner hook interfaces, `fa2_token_receiver` and `fa2_token_sender`, that need + to be implemented by token owner contracts to expose the owner's hooks to FA2 token + contract. + +* If a transfer failed because of the token owner permission behavior, the operation + MUST fail with the one of the following error mnemonics: + +| Error Mnemonic | Description | +| :------------- | :---------- | +| `"FA2_RECEIVER_HOOK_FAILED"` | Receiver hook is invoked and failed. This error MUST be raised by the hook implementation | +| `"FA2_SENDER_HOOK_FAILED"` | Sender hook is invoked and failed. This error MUST be raised by the hook implementation | +| `"FA2_RECEIVER_HOOK_UNDEFINED"` | Receiver hook is required by the permission behavior, but is not implemented by a receiver contract | +| `"FA2_SENDER_HOOK_UNDEFINED"` | Sender hook is required by the permission behavior, but is not implemented by a sender contract | + +* `transfer_descriptor` type defined below can represent regular transfer, mint and + burn operations. + +| operation | `from_` | `to_` | +| :-------- | :------ | :----- | +| transfer | MUST be `Some sender_address` | MUST be `Some receiver_address` | +| mint | MUST be `None` | MUST be `Some receiver_address` | +| burn | MUST be `Some burner_address` | MUST be `None` | + +* If all of the following conditions are met, the FA2 contract MUST invoke both + `fa2_token_sender` and `fa2_token_receiver` entry points: + * the token owner implements both `fa2_token_sender` and `fa2_token_receiver` + interfaces + * the token owner receives and sends some tokens in the same transfer operation + * both sender and receiver hooks are enabled by the FA2 permissions policy + +* If the token owner participates in multiple transfers within the transfer operation + batch and hook invocation is required by the permissions policy, the hook MUST + be invoked only once. + +* The hooks MUST NOT be invoked in the context of the operation other than transfer, + mint and burn. + +* `transfer_descriptor_param.operator` MUST be initialized with the address that + invoked the FA2 contract (`SENDER`). + +A special consideration is required if FA2 implementation supports sender and/or +receiver hooks. It is possible that one of the token owner hooks will fail because +of the hook implementation defects or other circumstances out of control of the +FA2 contract. This situation may cause tokens to be permanently locked on the token +owner's account. One of the possible solutions could be the implementation of a +special administrative version of the mint and burn operations that bypasses owner's +hooks otherwise required by the FA2 contract permissions policy. + +```ocaml +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type transfer_descriptor_param = { + batch : transfer_descriptor list; + operator : address; +} + +type fa2_token_receiver = + | Tokens_received of transfer_descriptor_param_michelson + +type fa2_token_sender = + | Tokens_sent of transfer_descriptor_param_michelson +``` + +
+where + +```ocaml +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param_aux = { + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb +``` + +
+ +Michelson definition: + +``` +(pair + (list %batch + (pair + (option %from_ address) + (list %txs + (pair + (option %to_ address) + (pair + (nat %token_id) + (nat %amount) + ) + ) + ) + ) + ) + (address %operator) +) +``` + +##### Permission Policy Formulae + +Each concrete implementation of the permission policy can be described by a formula +which combines permission behaviors in the following form: + +``` +Operator(?) * Receiver(?) * Sender(?) +``` + +For instance, `Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook)` +formula describes the policy which allows only token owners to transfer their own +tokens. + +`Operator(No_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook)` formula +represents non-transferable token (neither token owner, nor operators can transfer +tokens. + +Permission token policy formula is expressed by the `permissions_descriptor` type. + +```ocaml +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer + +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook + +type custom_permission_policy = { + tag : string; + config_api: address option; +} + +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} +``` + +It is possible to extend permission policy with a `custom` behavior, which does +not overlap with already existing standard policies. This standard does not specify +exact types for custom config entry points. FA2 token contract clients that support +custom config entry points must know their types a priori and/or use a `tag` hint +of `custom_permission_policy`. + +##### Customizing Permission Policy + +The FA2 contract MUST always implement the [core transfer behavior](#core-transfer-behavior). +However, FA2 contract developer MAY chose to implement either the +[default transfer permission policy](#default-transfer-permission-policy) or a +custom policy. +The FA2 contract implementation MAY customize one or more of the standard permission +behaviors (`operator`, `receiver`, `sender` as specified in `permissions_descriptor` +type), by choosing one of the available options for those permission behaviors. + +##### `permissions_descriptor` Entry Point + +LIGO definition: + +```ocaml +| Permissions_descriptor of permissions_descriptor_michelson contract +``` + +
+where + +```ocaml +type operator_transfer_policy_michelson = operator_transfer_policy michelson_or_right_comb + +type owner_hook_policy_michelson = owner_hook_policy michelson_or_right_comb + +type custom_permission_policy_michelson = custom_permission_policy michelson_pair_right_comb + +type permissions_descriptor_aux = { + operator : operator_transfer_policy_michelson; + receiver : owner_hook_policy_michelson; + sender : owner_hook_policy_michelson; + custom : custom_permission_policy_michelson option; +} + +type permissions_descriptor_michelson = permissions_descriptor_aux michelson_pair_right_comb +``` + +
+ +Michelson definition: + +``` +(contract %permissions_descriptor + (pair + (or %operator + (unit %no_transfer) + (or + (unit %owner_transfer) + (unit %owner_or_operator_transfer) + ) + ) + (pair + (or %receiver + (unit %owner_no_hook) + (or + (unit %optional_owner_hook) + (unit %required_owner_hook) + ) + ) + (pair + (or %sender + (unit %owner_no_hook) + (or + (unit %optional_owner_hook) + (unit %required_owner_hook) + ) + ) + (option %custom + (pair + (string %tag) + (option %config_api address) + ) + ) + ) + ) + ) +) +``` + +Get the descriptor of the transfer permission policy. FA2 specifies +`permissions_descriptor` allowing external contracts (e.g. an auction) to discover +an FA2 contract's implemented permission policies. + +The implicit value of the descriptor for the +[default `transfer` permission policy](#default-transfer-permission-policy) is +the following: + +```ocaml +let default_descriptor : permissions_descriptor = { + operator = Owner_or_operator_transfer; + receiver = Owner_no_hook; + sender = Owner_no_hook; + custom = (None: custom_permission_policy option); +} +``` + +* If the FA2 contract implements one or more non-default behaviors, it MUST implement + `permission_descriptor` entry point. The descriptor field values MUST reflect + actual permission behavior implemented by the contract. + +* If the FA2 contract implements the default permission policy, it MAY omit the + implementation of the `permissions_descriptor` entry point. + +* In addition to the standard permission behaviors, the FA2 contract MAY also + implement an optional custom permissions policy. If such custom a policy is + implemented, the FA2 contract SHOULD expose it using permissions descriptor + `custom` field. `custom_permission_policy.tag` value would be available to + other parties which are aware of such custom extension. Some custom permission + MAY require a config API (like [`update_operators`](#update_operators) entry + point of the FA2 to configure `operator_transfer_policy`). The address of the + contract that provides config entry points is specified by + `custom_permission_policy.config_api` field. The config entry points MAY be + implemented either within the FA2 token contract itself (then the returned + address SHALL be `SELF`), or in a separate contract. + +### Error Handling + +This specification defines the set of standard errors to make it easier to integrate +FA2 contracts with wallets, DApps and other generic software, and enable +localization of user-visible error messages. + +Each error code is a short abbreviated string mnemonic. An FA2 contract client +(like another contract or a wallet) could use on-the-chain or off-the-chain registry +to map the error code mnemonic to a user-readable, localized message. A particular +implementation of the FA2 contract MAY extend the standard set of errors with custom +mnemonics for additional constraints. + +Standard error mnemonics: + +| Error mnemonic | Description | +| :------------- | :---------- | +| `"FA2_TOKEN_UNDEFINED"` | One of the specified `token_id`s is not defined within the FA2 contract | +| `"FA2_INSUFFICIENT_BALANCE"` | A token owner does not have sufficient balance to transfer tokens from owner's account| +| `"FA2_TX_DENIED"` | A transfer failed because of `operator_transfer_policy == No_transfer` | +| `"FA2_NOT_OWNER"` | A transfer failed because `operator_transfer_policy == Owner_transfer` and it is invoked not by the token owner | +| `"FA2_NOT_OPERATOR"` | A transfer failed because `operator_transfer_policy == Owner_or_operator_transfer` and it is invoked neither by the token owner nor a permitted operator | +| `"FA2_OPERATORS_UNSUPPORTED"` | `update_operators` entry point is invoked and `operator_transfer_policy` is `No_transfer` or `Owner_transfer` | +| `"FA2_RECEIVER_HOOK_FAILED"` | The receiver hook failed. This error MUST be raised by the hook implementation | +| `"FA2_SENDER_HOOK_FAILED"` | The sender failed. This error MUST be raised by the hook implementation | +| `"FA2_RECEIVER_HOOK_UNDEFINED"` | Receiver hook is required by the permission behavior, but is not implemented by a receiver contract | +| `"FA2_SENDER_HOOK_UNDEFINED"` | Sender hook is required by the permission behavior, but is not implemented by a sender contract | + +If more than one error conditions are met, the entry point MAY fail with any applicable +error. + +When error occurs, any FA2 contract entry point MUST fail with one of the following +types: + +1. `string` value which represents an error code mnemonic. +2. a Michelson `pair`, where the first element is a `string` representing error code +mnemonic and the second element is a custom error data. + +Some FA2 implementations MAY introduce their custom errors that MUST follow the +same pattern as standard ones. + +## Implementing Different Token Types With FA2 + +The FA2 interface is designed to support a wide range of token types and implementations. +This section gives examples of how different types of the FA2 contracts MAY be +implemented and what are the expected properties of such an implementation. + +### Single Fungible Token + +An FA2 contract represents a single token similar to ERC-20 or FA1.2 standards. + +| Property | Constrains | +| :--------------- | :--------: | +| `token_id` | Always `0n` | +| transfer amount | natural number | +| account balance | natural number | +| total supply | natural number | +| decimals | custom | + +### Multiple Fungible Tokens + +An FA2 contract may represent multiple tokens similar to ERC-1155 standard. +The implementation can have a fixed predefined set of supported tokens or tokens +can be created dynamically. + +| Property | Constrains | +| :--------------- | :--------: | +| `token_id` | natural number | +| transfer amount | natural number | +| account balance | natural number | +| total supply | natural number | +| decimals | custom, per each `token_id` | + +### Non-fungible Tokens + +An FA2 contract may represent non-fungible tokens (NFT) similar to ERC-721 standard. +For each individual non-fungible token the implementation assigns a unique `token_id`. +The implementation MAY support either a single kind of NFTs or multiple kinds. +If multiple kinds of NFT is supported, each kind MAY be assigned a continuous range +of natural number (that does not overlap with other ranges) and have its own associated +metadata. + +| Property | Constrains | +| :--------------- | :--------: | +| `token_id` | natural number | +| transfer amount | `0n` or `1n` | +| account balance | `0n` or `1n` | +| total supply | `0n` or `1n` | +| decimals | `0n` or a natural number if a token represents a batch of items | + +For any valid `token_id` only one account CAN hold the balance of one token (`1n`). +The rest of the accounts MUST hold zero balance (`0n`) for that `token_id`. + +### Mixing Fungible and Non-fungible Tokens + +An FA2 contract MAY mix multiple fungible and non-fungible tokens within the same +contract similar to ERC-1155. The implementation MAY chose to select individual +natural numbers to represent `token_id` for fungible tokens and continuous natural +number ranges to represent `token_id`s for NFTs. + +| Property | Constrains | +| :--------------- | :--------: | +| `token_id` | natural number | +| transfer amount | `0n` or `1n` for NFT and natural number for fungible tokens | +| account balance | `0n` or `1n` for NFT and natural number for fungible tokens | +| total supply | `0n` or `1n` for NFT and natural number for fungible tokens | +| decimals | custom | + +### Non-transferable Tokens + +Either fungible and non-fungible tokens can be non-transferable. Non-transferable +tokens can be represented by the FA2 contract which [operator transfer behavior](#operator-transfer-behavior) +is defined as `No_transfer`. Tokens cannot be transferred neither by the token owner +nor by any operator. Only privileged operations like mint and burn can assign tokens +to owner accounts. + +## Future Directions + +Future amendments to Tezos are likely to enable new functionality by which this +standard can be upgraded. Namely, [read-only +calls](https://forum.tezosagora.org/t/adding-read-only-calls/1227), event logging, +and [contract signatures](https://forum.tezosagora.org/t/contract-signatures/1458), now known as "tickets". + +## Copyright + +Copyright and related rights waived via +[CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution_expanded.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution_expanded.cmd new file mode 100644 index 0000000..337db01 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution_expanded.cmd @@ -0,0 +1,137 @@ +ligo compile-storage shiptoken.mligo main +'{ + paused=false; + entities=Map.literal [((1n,0n),{name="first";code="040233"})]; + entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; + owner_entities=Map.literal [ + (("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); + (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); + (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set)) + ]; + tokens=Map.literal [ + (0n,{ + total_supply=1n; + metadata={ + token_id=0n; + symbol="<3"; + name="TzAcademyShip"; + decimals=0n; + extras=(Map.empty :(string, string) map) + } + }) + ]; + operators=(Set.empty : operator_param set); + administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); + permissions_descriptor={ + operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); + receiver=Layout.convert_to_right_comb(Owner_no_hook); + sender=Layout.convert_to_right_comb(Owner_no_hook); + custom=(None : custom_permission_policy_michelson option) + } +}' + + +ligo dry-run --sender=tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J shiptoken.mligo main +'Fa2 ( + Update_operators([ + ( + Layout.convert_to_right_comb( + Add_operator( + ( + Layout.convert_to_right_comb( + ({ + owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address) + }: operator_param) + ) : operator_param_michelson + ) + ) + ) : update_operator_michelson) + ]) +)' +'{ + paused=false; + entities=Map.literal [((1n,0n),{name="first";code="040233"})]; + entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; + owner_entities=Map.literal [ + (("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); + (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); + (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set)) + ]; + tokens=Map.literal [ + (0n,{ + total_supply=1n; + metadata={ + token_id=0n; + symbol="<3"; + name="TzAcademyShip"; + decimals=0n; + extras=(Map.empty :(string, string) map) + } + }) + ]; + operators=(Set.empty : operator_param set); + administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); + permissions_descriptor={ + operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); + receiver=Layout.convert_to_right_comb(Owner_no_hook); + sender=Layout.convert_to_right_comb(Owner_no_hook); + custom=(None : custom_permission_policy_michelson option) + } +}' + + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU shiptoken.mligo main +'Fa2 ( + Transfer( [ + (Layout.convert_to_right_comb( + ({ + from_=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + txs=[ + (Layout.convert_to_right_comb( + ({ + to_=("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address); + token_id=0n; + amount=1n + }: transfer_destination) + ) : transfer_destination_michelson) + ] + } : transfer_aux) + ): transfer_michelson) + ]) +)' +'{ + paused=false; + entities=Map.literal [((1n,0n),{name="first";code="040233"})]; + entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; + owner_entities=Map.literal [ + (("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); + (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); + (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set)) + ]; + tokens=Map.literal [ + (0n,{ + total_supply=1n; + metadata={ + token_id=0n; + symbol="<3"; + name="TzAcademyShip"; + decimals=0n; + extras=(Map.empty :(string, string) map) + } + }) + ]; + operators=Set.add + ({ + owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address) + }: operator_param) + (Set.empty : operator_param set); + administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); + permissions_descriptor={ + operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); + receiver=Layout.convert_to_right_comb(Owner_no_hook); + sender=Layout.convert_to_right_comb(Owner_no_hook); + custom=(None : custom_permission_policy_michelson option) + } +}' From 2795f66139ae5eac91345f4cc495e062deb26bed Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Thu, 11 Jun 2020 00:19:00 +0200 Subject: [PATCH 09/16] [Cameligo] fa2 implementation + course.md; separate FA2 chapter into 3 parts --- .../Chapters/Camel/ChapterFA20/caller.mligo | 53 ++++ .../Chapters/Camel/ChapterFA20/course.md | 255 ++++++++++++++++++ .../Chapters/Camel/ChapterFA20/exercise.mligo | 154 +++++++++++ .../ChapterFA20/tzip-12/fa2_errors.mligo | 44 +++ .../Camel/ChapterFA20/tzip-12/fa2_hook.mligo | 24 ++ .../ChapterFA20/tzip-12/fa2_interface.mligo | 219 +++++++++++++++ .../tzip-12/lib/fa2_behaviors.mligo | 87 ++++++ .../tzip-12/lib/fa2_convertors.mligo | 187 +++++++++++++ .../tzip-12/lib/fa2_hook_lib.mligo | 40 +++ .../tzip-12/lib/fa2_operator_lib.mligo | 64 +++++ .../Chapters/Camel/ChapterFA20Hook/course.md | 210 +++++++++++++++ .../Camel/ChapterFA20Permission/course.md | 164 +++++++++++ 12 files changed, 1501 insertions(+) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_errors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_hook.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_interface.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_behaviors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_convertors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_hook_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_operator_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo new file mode 100644 index 0000000..a4d6843 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo @@ -0,0 +1,53 @@ +#include "tzip/proposals/tzip-12/lib/fa2_hook_lib.mligo" +#include "tzip/proposals/tzip-12/lib/fa2_behaviors.mligo" + +type storage = { + rep : balance_of_response_michelson list +} + +type request_balance_of_param = { + at : address; + requests : balance_of_request list; +} + +type receive_balance_of_param = balance_of_response_michelson list + +let request_balance (req, s : (request_balance_of_param * storage)) : operation list * storage = + // Get the TZIP-12 contract instance 'from' the chain + let token_contract_balance_entrypoint_opt : balance_of_param contract option = Tezos.get_entrypoint_opt "%balance_of" req.at in + let token_contract_balance_entrypoint : balance_of_param contract = match (token_contract_balance_entrypoint_opt) with + | Some (ci) -> ci + | None -> (failwith("Entrypoint not found"): balance_of_param contract) + in + + // Callback (contract) for Balance_of will be the current contract's Receive_balance entrypoint + let balance_of_callback_contract_opt : receive_balance_of_param contract option = Tezos.get_entrypoint_opt "%receive_balance" Tezos.self_address in + let balance_of_callback_contract : receive_balance_of_param contract = match (balance_of_callback_contract_opt) with + | Some (ci) -> ci + | None -> (failwith("Entrypoint not found"): receive_balance_of_param contract) + in + //let balance_of_callback_contract : receive_balance_of_param contract = get_entrypoint("%receive_balance", Tezos.self_address) in + // Set up the parameter w/ data required for the Balance_of entrypoint + let balance_of_operation_param : balance_of_param = { + requests = req.requests; + callback = balance_of_callback_contract; + } in + // Forge an internal transaction to the TZIP-12 contract + // parametrised by the prieviously prepared `balance_of_operation_param` + // Note: We're sending 0mutez as part of this transaction + let balance_of_operation : operation = Tezos.transaction balance_of_operation_param 0mutez token_contract_balance_entrypoint in + ([ balance_of_operation ], s) + +let receive_balance (received, s: (receive_balance_of_param * storage)) : operation list * storage = + let new_store : storage = { s with rep = received } in + (([] : operation list), new_store) + +type entry_points = + | Request_balance of request_balance_of_param + | Receive_balance of receive_balance_of_param + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Request_balance request_balance_of_param -> request_balance (request_balance_of_param, s) + | Receive_balance receive_balance_of_param -> receive_balance (receive_balance_of_param, s) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md new file mode 100644 index 0000000..61a809e --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md @@ -0,0 +1,255 @@ +# Chapter 23 : Financial Asset 2.0 + +Captain, Let's create a ship token. + +## Definition + +There are multiple dimensions and considerations while implementing a particular token smart contract. Tokens might be fungible or non-fungible. A variety of +permission policies can be used to define how many tokens can be transferred, who can initiate a transfer, and who can receive tokens. A token contract can be +designed to support a single token type (e.g. ERC-20 or ERC-721) or multiple token types (e.g. ERC-1155) to optimize batch transfers and atomic swaps of the tokens. + +The FA2 standard proposes a *unified token contract interface* that accommodates all mentioned concerns. It aims to provide significant expressivity to contract developers to create new types of tokens while maintaining a common interface standard for wallet integrators and external developers. + +In the following chapter on Financial Asset 2.0 , we will focus on *TZIP-12* which stands for the 12th Tezos Improvement Proposal (same as EIP-721 for Ethereum). + +## Interface and library + +The FA2 interface formalize a standard way to design tokens and thus describes a list of entry points (that must be implemented) and data structures related to those entry points. A more detailed decription of the interface is broken down in following sections. + +In addition to the FA2 interface, the FA2 standard provides helper functions to manipulate data structures involved in FA2 interface. The FA2 library contains helper functions for : +* a generic behavior and transfer hook implementation (behavior based on *permissions_descriptor*), +* converting data structures, +* defining hooks between contracts when transfer is emitted, +* defining operators for managing allowance. + +## Entry points + +Token contract implementing the FA2 standard MUST have the following entry points. + +``` +type fa2_entry_points = + +| Transfer of transfer list +| Balance_of of balance_of_param +| Total_supply of total_supply_param +| Token_metadata of token_metadata_param +| Permissions_descriptor of permissions_descriptor contract +| Update_operators of update_operator list +| Is_operator of is_operator_param +``` + + +### Metadata + +FA2 token contracts MUST implement the *token_metadata* entry point which get the metadata for multiple token types. + +It accepts a list of *token_ids* and a callback contract, and sends back a list of *token_metadata* records. + +FA2 token amounts are represented by natural numbers (nat), and their granularity (the smallest amount of tokens which may be minted, burned, or +transferred) is always 1. + +The *decimals* property is the number of digits to use after the decimal point when displaying the token amounts. If 0, the asset is not divisible. Decimals are used for display purposes only and MUST NOT affect transfer operation. + + +#### Interface + +``` +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} + +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param = { + token_ids : token_id list; + callback : (token_metadata_michelson list) contract; +} +``` + + +### Balance of + +FA2 token contracts MUST implement the _Balance of_ entry point which get the balance of multiple account/token pairs (because FA2 supports mutiple token). +``` +| Balance_of of balance_of_param +``` + +It accepts a list of balance_of_requests and a callback and sends back to a callback contract a list of balance_of_response records. + +If one of the specified token_ids is not defined within the FA2 contract, the entry point MUST fail with the error mnemonic "TOKEN_UNDEFINED" (see section Error Handling). + +#### Interface + +The FA2 interface defines request/response parameters as follow : +``` +type token_id = nat + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} +``` + +### Totalsupply + +FA2 token contracts MUST implement the _Totalsupply_ entry point which get the total supply of tokens for multiple token types (because FA2 supports mutiple token). +``` +| Total_supply of total_supply_param +``` + +It accepts a list of *token_ids* and a callback, and sends back to the callback contract a list of *total_supply_response* records. + +If one of the specified token_ids is not defined within the FA2 contract, the entry point MUST fail with the error mnemonic "TOKEN_UNDEFINED" (see section Error Handling). + +#### Interface + +``` +type token_id = nat + +type total_supply_response = { + token_id : token_id; + total_supply : nat; +} + +type total_supply_response_michelson = total_supply_response michelson_pair_right_comb + +type total_supply_param = { + token_ids : token_id list; + callback : (total_supply_response_michelson list) contract; +} +``` + +### Transfer + +FA2 token contracts MUST implement the _Transfer_ entry point which transfer tokens between and MUST ensure following rules. +``` +| Transfer of transfer list +``` + +#### Rules + +FA2 token contracts MUST implement the transfer logic defined by the following rules : + + +1) Every transfer operation MUST be atomic. If the operation fails, all token transfers MUST be reverted, and token balances MUST remain unchanged. + +2) The amount of a token transfer MUST NOT exceed the existing token owner's balance. If the transfer amount for the particular token type and token owner +exceeds the existing balance, the whole transfer operation MUST fail with the error mnemonic "INSUFFICIENT_BALANCE" + +3) Core transfer behavior MAY be extended. If additional constraints on tokens transfer are required, FA2 token contract implementation MAY invoke additional +permission policies (transfer hook is the recommended design pattern to implement core behavior extension). (See Chapter FA2 - Hook) + +If the additional permission hook fails, the whole transfer operation MUST fail with a custom error mnemonic. + +4) Core transfer behavior MUST update token balances exactly as the operation parameters specify it. No changes to amount values or additional transfers are +allowed. + + + +#### Interface + +It transfer tokens from a *from_* account to possibly many destination accounts where each destination transfer describes the type of token, the amount of token, and receiver address. + +``` +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + + +``` + +### Error Handling + +This FA2 tandard defines the set of standard errors to make it easier to integrate FA2 contracts with wallets, DApps and other generic software, and enable +localization of user-visible error messages. + +Each error code is a short abbreviated string mnemonic. An FA2 contract client (like another contract or a wallet) could use on-the-chain or off-the-chain registry to map the error code mnemonic to a user-readable, localized message. + +A particular implementation of the FA2 contract MAY extend the standard set of errors with custom mnemonics for additional constraints. + +When error occurs, any FA2 contract entry point MUST fail with one of the following types: +* string value which represents an error code mnemonic. +* a Michelson pair, where the first element is a string representing error code mnemonic and the second element is a custom error data. + +#### Standard error mnemonics: + +Error mnemonic +Description + + + + +"TOKEN_UNDEFINED" +One of the specified token_ids is not defined within the FA2 contract + + +"INSUFFICIENT_BALANCE" +A token owner does not have sufficient balance to transfer tokens from owner's account + + +"TX_DENIED" +A transfer failed because of operator_transfer_policy == No_transfer + + + +"NOT_OWNER" +A transfer failed because operator_transfer_policy == Owner_transfer and it is initiated not by the token owner + + +"NOT_OPERATOR" +A transfer failed because operator_transfer_policy == Owner_or_operator_transfer and it is initiated neither by the token owner nor a permitted operator + + +"RECEIVER_HOOK_FAILED" +The receiver hook failed. This error MUST be raised by the hook implementation + + +"SENDER_HOOK_FAILED" +The sender failed. This error MUST be raised by the hook implementation + + +"RECEIVER_HOOK_UNDEFINED" +Receiver hook is required by the permission behavior, but is not implemented by a receiver contract + + +"SENDER_HOOK_UNDEFINED" +Sender hook is required by the permission behavior, but is not implemented by a sender contract + + + + +## Your mission + + +1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. + diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo new file mode 100644 index 0000000..fca09fd --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo @@ -0,0 +1,154 @@ +#include "tzip/proposals/tzip-12/fa2_interface.mligo" +#include "tzip/proposals/tzip-12/fa2_errors.mligo" + +type entity = { + balance : nat +} +type entity_key = address * token_id +type ledger = (entity_key, entity) map + +type tokens = { + total_supply : nat; + metadata : token_metadata; +} + +type operator_ = { + owner : address; + operator : address; +} + +type storage = { + paused : bool; + ledger : ledger; + tokens : (nat,tokens) map; + operators : operator_ set; + administrator : address; +} + +type return = (operation list * storage) + +type entry_points = + | Set_pause of bool + | Set_administrator of address + | MyBalance_of of balance_of_param + | MyTotal_supply of total_supply_param_michelson + | MyToken_metadata of token_metadata_param_michelson + | MyTransfer of transfer_michelson list + +let set_pause(param,s : bool * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with paused=param }) + else + (failwith("only admin can do it") : return) + +let set_administrator(param,s : address * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with administrator=param }) + else + (failwith("only admin can do it") : return) + +let balance_of (balance_of_param, s : balance_of_param * storage) : return = + let get_balance = fun ( i : balance_of_request) -> match Map.find_opt (i.owner,i.token_id) s.ledger with + | Some e -> { request = i ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response) + in + let balance_of_callback_param : balance_of_response list = List.map get_balance balance_of_param.requests in + let convert = fun ( r : balance_of_response) -> Layout.convert_to_right_comb(({request=Layout.convert_to_right_comb( r.request ); balance=r.balance} : balance_of_response_aux)) in + let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in + // sending back the processed map of balance requests/responses + let destination: (balance_of_response_michelson list) contract = balance_of_param.callback in + let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in + ([balance_of_response_operation], s) + +let total_supply(params, s: total_supply_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith("unknown token_id") : total_supply_response) + in + let responses : total_supply_response list = List.map get_total_supply token_ids in + let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in + let ret : total_supply_response_michelson list = List.map convert responses in + let destination: (total_supply_response_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let token_metadata(params, s: token_metadata_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : token_metadata_param = Layout.convert_from_right_comb(params: token_metadata_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> v.metadata + | None -> (failwith("unknown token_id") : token_metadata) + in + let responses : token_metadata list = List.map get_metadata token_ids in + let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in + let ret : token_metadata_michelson list = List.map convert responses in + let destination: (token_metadata_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let transfer(params, s: transfer_michelson list * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let apply_transfer = fun (l,i : ledger * transfer_michelson) -> + let t_aux : transfer_aux = Layout.convert_from_right_comb(i: transfer_michelson) in + let from_ : address = t_aux.from_ in + let result_ledger : ledger = if Tezos.sender = from_ or Tezos.sender = s.administrator then + if Set.mem {owner=from_;operator=Tezos.sender} s.operators then + let transfers : transfer_destination_michelson list = t_aux.txs in + let apply_transfer_destination = fun (acc,j : (ledger * transfer_destination_michelson)) -> + let transfer_destination : transfer_destination = Layout.convert_from_right_comb(j: transfer_destination_michelson) in + let tr_amount : nat = transfer_destination.amount in + let to_ : address = transfer_destination.to_ in + let tokenid : token_id = transfer_destination.token_id in + let temp_state_ledger : ledger = if tr_amount > 0n then + let enough_funds : bool = match Map.find_opt (from_,tokenid) acc with + | Some bal -> (bal.balance >= tr_amount) + | None -> false + in + if enough_funds then + let l_updated_from : ledger = match Map.find_opt (from_,tokenid) acc with + | Some bal -> Map.update (from_,tokenid) (Some {balance=abs(bal.balance - tr_amount)} ) acc + | None -> (failwith("should not arrive here") : ledger) + in + let l_updated_from_to : ledger = match Map.find_opt (to_,tokenid) l_updated_from with + | Some bal -> Map.update (to_,tokenid) (Some {balance=bal.balance + tr_amount}) l_updated_from + | None -> Map.add (to_,tokenid) {balance=tr_amount} l_updated_from + in + l_updated_from_to + else + (failwith(insufficient_balance) : ledger) + else + (failwith("transferring nothing !") : ledger) + in + temp_state_ledger + in + let result : ledger = List.fold apply_transfer_destination transfers l in + result + else + (failwith(not_operator) : ledger) + else + (failwith(not_owner) : ledger) + in + result_ledger + in + let new_ledger : ledger = List.fold apply_transfer params s.ledger in + (([] : operation list), new_ledger) + + +let main (p,s : entry_points * storage) : return = + match p with + | MyTotal_supply p -> total_supply (p,s) + | MyBalance_of p -> balance_of (p,s) + | MyToken_metadata p -> token_metadata (p,s) + | Set_pause p -> set_pause (p,s) + | Set_administrator p -> set_administrator (p,s) + | MyTransfer l -> transfer (l, s) \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_errors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_errors.mligo new file mode 100644 index 0000000..506f2e8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_errors.mligo @@ -0,0 +1,44 @@ +#if !FA2_ERRORS +#define FA2_ERRORS + +(** One of the specified `token_id`s is not defined within the FA2 contract *) +let token_undefined = "TOKEN_UNDEFINED" +(** +A token owner does not have sufficient balance to transfer tokens from +owner's account +*) +let insufficient_balance = "INSUFFICIENT_BALANCE" +(** A transfer failed because of `operator_transfer_policy == No_transfer` *) +let tx_denied = "TX_DENIED" +(** +A transfer failed because `operator_transfer_policy == Owner_transfer` and it is +initiated not by the token owner +*) +let not_owner = "NOT_OWNER" +(** +A transfer failed because `operator_transfer_policy == Owner_or_operator_transfer` +and it is initiated neither by the token owner nor a permitted operator + *) +let not_operator = "NOT_OPERATOR" +(** +Receiver hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let receiver_hook_failed = "RECEIVER_HOOK_FAILED" +(** +Sender hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let sender_hook_failed = "SENDER_HOOK_FAILED" +(** +Receiver hook is required by the permission behavior, but is not implemented by +a receiver contract + *) +let receiver_hook_undefined = "RECEIVER_HOOK_UNDEFINED" +(** +Sender hook is required by the permission behavior, but is not implemented by +a sender contract + *) +let sender_hook_undefined = "SENDER_HOOK_UNDEFINED" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_hook.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_hook.mligo new file mode 100644 index 0000000..6137386 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_hook.mligo @@ -0,0 +1,24 @@ + +#if !FA2_HOOK +#define FA2_HOOK + +#include "fa2_interface.mligo" + + +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb + +type fa2_with_hook_entry_points = + | Fa2 of fa2_entry_points + | Set_transfer_hook of set_hook_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_interface.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_interface.mligo new file mode 100644 index 0000000..c6043a0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/fa2_interface.mligo @@ -0,0 +1,219 @@ +#if ! FA2_INTERFACE +#define FA2_INTERFACE + +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + +type transfer_michelson = transfer_aux michelson_pair_right_comb + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_request_michelson = balance_of_request michelson_pair_right_comb + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_response_aux = { + request : balance_of_request_michelson; + balance : nat; +} + +type balance_of_response_michelson = balance_of_response_aux michelson_pair_right_comb + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_aux = { + requests : balance_of_request_michelson list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_michelson = balance_of_param_aux michelson_pair_right_comb + +type total_supply_response = { + token_id : token_id; + total_supply : nat; +} + +type total_supply_response_michelson = total_supply_response michelson_pair_right_comb + +type total_supply_param = { + token_ids : token_id list; + callback : (total_supply_response_michelson list) contract; +} + +type total_supply_param_michelson = total_supply_param michelson_pair_right_comb + +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} + +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param = { + token_ids : token_id list; + callback : (token_metadata_michelson list) contract; +} + +type token_metadata_param_michelson = token_metadata_param michelson_pair_right_comb + +type operator_param = { + owner : address; + operator : address; +} + +type operator_param_michelson = operator_param michelson_pair_right_comb + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type update_operator_aux = + | Add_operator of operator_param_michelson + | Remove_operator of operator_param_michelson + +type update_operator_michelson = update_operator_aux michelson_or_right_comb + +type is_operator_response = { + operator : operator_param; + is_operator : bool; +} + +type is_operator_response_aux = { + operator : operator_param_michelson; + is_operator : bool; +} + +type is_operator_response_michelson = is_operator_response_aux michelson_pair_right_comb + +type is_operator_param = { + operator : operator_param; + callback : (is_operator_response_michelson) contract; +} + +type is_operator_param_aux = { + operator : operator_param_michelson; + callback : (is_operator_response_michelson) contract; +} + +type is_operator_param_michelson = is_operator_param_aux michelson_pair_right_comb + +(* permission policy definition *) + +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer + +type operator_transfer_policy_michelson = operator_transfer_policy michelson_or_right_comb + +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook + +type owner_hook_policy_michelson = owner_hook_policy michelson_or_right_comb + +type custom_permission_policy = { + tag : string; + config_api: address option; +} + +type custom_permission_policy_michelson = custom_permission_policy michelson_pair_right_comb + +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} + +type permissions_descriptor_aux = { + operator : operator_transfer_policy_michelson; + receiver : owner_hook_policy_michelson; + sender : owner_hook_policy_michelson; + custom : custom_permission_policy_michelson option; +} + +type permissions_descriptor_michelson = permissions_descriptor_aux michelson_pair_right_comb + +type fa2_entry_points = + | Transfer of transfer_michelson list + | Balance_of of balance_of_param_michelson + | Total_supply of total_supply_param_michelson + | Token_metadata of token_metadata_param_michelson + | Permissions_descriptor of permissions_descriptor_michelson contract + | Update_operators of update_operator_michelson list + | Is_operator of is_operator_param_michelson + + +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param = { + fa2 : address; + batch : transfer_descriptor list; + operator : address; +} + +type transfer_descriptor_param_aux = { + fa2 : address; + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb + +type fa2_token_receiver = + | Tokens_received of transfer_descriptor_param_michelson + +type fa2_token_sender = + | Tokens_sent of transfer_descriptor_param_michelson + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_behaviors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_behaviors.mligo new file mode 100644 index 0000000..dd933ac --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_behaviors.mligo @@ -0,0 +1,87 @@ +#if !FA2_BEHAVIORS +#define FA2_BEHAVIORS + +(* #include "fa2_hook_lib.mligo" *) +#include "../fa2_interface.mligo" +#include "../fa2_errors.mligo" + + +(** generic transfer hook implementation. Behavior is driven by `permissions_descriptor` *) + +type get_owners = transfer_descriptor -> (address option) list +type to_hook = address -> ((transfer_descriptor_param_michelson contract) option * string) +type transfer_hook_params = { + ligo_param : transfer_descriptor_param; + michelson_param : transfer_descriptor_param_michelson; +} + +let get_owners_from_batch (batch, get_owners : (transfer_descriptor list) * get_owners) : address set = + List.fold + (fun (acc, tx : (address set) * transfer_descriptor) -> + let owners = get_owners tx in + List.fold + (fun (acc, o: (address set) * (address option)) -> + match o with + | None -> acc + | Some a -> Set.add a acc + ) + owners + acc + ) + batch + (Set.empty : address set) + +let validate_owner_hook (p, get_owners, to_hook, is_required : + transfer_hook_params * get_owners * to_hook * bool) + : operation list = + let owners = get_owners_from_batch (p.ligo_param.batch, get_owners) in + Set.fold + (fun (ops, owner : (operation list) * address) -> + let hook, error = to_hook owner in + match hook with + | Some h -> + let op = Operation.transaction p.michelson_param 0mutez h in + op :: ops + | None -> + if is_required + then (failwith error : operation list) + else ops) + owners ([] : operation list) + +let validate_owner(p, policy, get_owners, to_hook : + transfer_hook_params * owner_hook_policy * get_owners * to_hook) + : operation list = + match policy with + | Owner_no_hook -> ([] : operation list) + | Optional_owner_hook -> validate_owner_hook (p, get_owners, to_hook, false) + | Required_owner_hook -> validate_owner_hook (p, get_owners, to_hook, true) + +let to_receiver_hook : to_hook = fun (a : address) -> + let c : (transfer_descriptor_param_michelson contract) option = + Operation.get_entrypoint_opt "%tokens_received" a in + c, receiver_hook_undefined + +let validate_receivers (p, policy : transfer_hook_params * owner_hook_policy) + : operation list = + let get_receivers : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_ )tx.txs in + validate_owner (p, policy, get_receivers, to_receiver_hook) + +let to_sender_hook : to_hook = fun (a : address) -> + let c : (transfer_descriptor_param_michelson contract) option = + Operation.get_entrypoint_opt "%tokens_sent" a in + c, sender_hook_undefined + +let validate_senders (p, policy : transfer_hook_params * owner_hook_policy) + : operation list = + let get_sender : get_owners = fun (tx : transfer_descriptor) -> [tx.from_] in + validate_owner (p, policy, get_sender, to_sender_hook) + +let standard_transfer_hook (p, descriptor : transfer_hook_params * permissions_descriptor) + : operation list = + let sender_ops = validate_senders (p, descriptor.sender) in + let receiver_ops = validate_receivers (p, descriptor.receiver) in + (* merge two lists *) + List.fold (fun (l, o : (operation list) * operation) -> o :: l) receiver_ops sender_ops + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_convertors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_convertors.mligo new file mode 100644 index 0000000..9cc7c6c --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_convertors.mligo @@ -0,0 +1,187 @@ +#if !FA2_CONVERTORS +#define FA2_CONVERTORS + +#include "../fa2_interface.mligo" + +let permissions_descriptor_to_michelson (d : permissions_descriptor) + : permissions_descriptor_michelson = + let aux : permissions_descriptor_aux = { + operator = Layout.convert_to_right_comb d.operator; + receiver = Layout.convert_to_right_comb d.receiver; + sender = Layout.convert_to_right_comb d.sender; + custom = match d.custom with + | None -> (None : custom_permission_policy_michelson option) + | Some c -> Some (Layout.convert_to_right_comb c) + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_to_michelson (p : transfer_descriptor) : transfer_descriptor_michelson = + let aux : transfer_descriptor_aux = { + from_ = p.from_; + txs = List.map + (fun (tx : transfer_destination_descriptor) -> + Layout.convert_to_right_comb tx + ) + p.txs; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_param_to_michelson (p : transfer_descriptor_param) + : transfer_descriptor_param_michelson = + let aux : transfer_descriptor_param_aux = { + fa2 = p.fa2; + operator = p.operator; + batch = List.map + (fun (td: transfer_descriptor) -> transfer_descriptor_to_michelson td) + p.batch; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_from_michelson (p : transfer_descriptor_michelson) : transfer_descriptor = + let aux : transfer_descriptor_aux = Layout.convert_from_right_comb p in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_descriptor_michelson) -> + let tx : transfer_destination_descriptor = + Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfer_descriptor_param_from_michelson (p : transfer_descriptor_param_michelson) + : transfer_descriptor_param = + let aux : transfer_descriptor_param_aux = Layout.convert_from_right_comb p in + let b : transfer_descriptor list = List.map + (fun (tdm : transfer_descriptor_michelson) -> + transfer_descriptor_from_michelson tdm + ) + aux.batch + in + { + fa2 = aux.fa2; + operator = aux.operator; + batch = b; + } + +let transfer_from_michelson (txm : transfer_michelson) : transfer = + let aux : transfer_aux = Layout.convert_from_right_comb txm in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_michelson) -> + let tx : transfer_destination = Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfers_from_michelson (txsm : transfer_michelson list) : transfer list = + List.map + (fun (txm: transfer_michelson) -> + let tx : transfer = transfer_from_michelson txm in + tx + ) txsm + +let operator_param_from_michelson (p : operator_param_michelson) : operator_param = + let op : operator_param = Layout.convert_from_right_comb p in + op + +let operator_param_to_michelson (p : operator_param) : operator_param_michelson = + Layout.convert_to_right_comb p + +let operator_update_from_michelson (uom : update_operator_michelson) : update_operator = + let aux : update_operator_aux = Layout.convert_from_right_comb uom in + match aux with + | Add_operator opm -> Add_operator_p (operator_param_from_michelson opm) + | Remove_operator opm -> Remove_operator_p (operator_param_from_michelson opm) + +let operator_update_to_michelson (uo : update_operator) : update_operator_michelson = + let aux = match uo with + | Add_operator_p op -> Add_operator (operator_param_to_michelson op) + | Remove_operator_p op -> Remove_operator (operator_param_to_michelson op) + in + Layout.convert_to_right_comb aux + +(* check this *) +let operator_updates_from_michelson (updates_michelson : update_operator_michelson list) + : update_operator list = + List.map operator_update_from_michelson updates_michelson + +let is_operator_param_from_michelson (p : is_operator_param_michelson) : is_operator_param = + let aux : is_operator_param_aux = Layout.convert_from_right_comb p in + { + operator = operator_param_from_michelson aux.operator; + callback = aux.callback; + } + +let is_operator_param_to_michelson (p : is_operator_param) : is_operator_param_michelson = + let aux : is_operator_param_aux = + { + operator = operator_param_to_michelson p.operator; + callback = p.callback; + } in + Layout.convert_to_right_comb aux + +let is_operator_response_to_michelson (r : is_operator_response) : is_operator_response_michelson = + let aux : is_operator_response_aux = { + operator = operator_param_to_michelson r.operator; + is_operator = r.is_operator; + } in + Layout.convert_to_right_comb aux + +let balance_of_param_from_michelson (p : balance_of_param_michelson) : balance_of_param = + let aux : balance_of_param_aux = Layout.convert_from_right_comb p in + let requests = List.map + (fun (rm : balance_of_request_michelson) -> + let r : balance_of_request = Layout.convert_from_right_comb rm in + r + ) + aux.requests + in + { + requests = requests; + callback = aux.callback; + } + +let balance_of_param_to_michelson (p : balance_of_param) : balance_of_param_michelson = + let aux : balance_of_param_aux = { + requests = List.map + (fun (r : balance_of_request) -> Layout.convert_to_right_comb r) + p.requests; + callback = p.callback; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_to_michelson (r : balance_of_response) : balance_of_response_michelson = + let aux : balance_of_response_aux = { + request = Layout.convert_to_right_comb r.request; + balance = r.balance; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_from_michelson (rm : balance_of_response_michelson) : balance_of_response = + let aux : balance_of_response_aux = Layout.convert_from_right_comb rm in + let request : balance_of_request = Layout.convert_from_right_comb aux.request in + { + request = request; + balance = aux.balance; + } + +let total_supply_responses_to_michelson (rs : total_supply_response list) + : total_supply_response_michelson list = + List.map + (fun (r : total_supply_response) -> + let rm : total_supply_response_michelson = Layout.convert_to_right_comb r in + rm + ) rs + +let token_metas_to_michelson (ms : token_metadata list) : token_metadata_michelson list = + List.map + ( fun (m : token_metadata) -> + let mm : token_metadata_michelson = Layout.convert_to_right_comb m in + mm + ) ms + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_hook_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_hook_lib.mligo new file mode 100644 index 0000000..de1e3c1 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_hook_lib.mligo @@ -0,0 +1,40 @@ +#if !FA2_HOOK_LIB +#define FA2_HOOK_LIB + +#include "../fa2_hook.mligo" +#include "fa2_convertors.mligo" + +let get_hook_entrypoint (hook_contract : address) (u : unit) + : transfer_descriptor_param_michelson contract = + let hook_entry : transfer_descriptor_param_michelson contract = + Operation.get_entrypoint "%tokens_transferred_hook" hook_contract in + hook_entry + + +let create_register_hook_op + (fa2, descriptor : (fa2_with_hook_entry_points contract) * permissions_descriptor) : operation = + let hook_fn = get_hook_entrypoint Current.self_address in + let p : set_hook_param_aux = { + hook = hook_fn; + permissions_descriptor = permissions_descriptor_to_michelson descriptor; + } in + let pm = Layout.convert_to_right_comb p in + Operation.transaction (Set_transfer_hook pm) 0mutez fa2 + + +type fa2_registry = address set + +let register_with_fa2 (fa2, descriptor, registry : + (fa2_with_hook_entry_points contract) * permissions_descriptor * fa2_registry) + : operation * fa2_registry = + let op = create_register_hook_op (fa2, descriptor) in + let fa2_address = Current.address fa2 in + let new_registry = Set.add fa2_address registry in + op, new_registry + +let validate_hook_call (fa2, registry: address * fa2_registry) : unit = + if Set.mem fa2 registry + then unit + else failwith "UNKNOWN_FA2_CALL" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_operator_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_operator_lib.mligo new file mode 100644 index 0000000..4872180 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/tzip-12/lib/fa2_operator_lib.mligo @@ -0,0 +1,64 @@ +(** Reference implementation of the FA2 operator storage and config API functions *) +#if !FA2_OPERATOR_LIB +#define FA2_OPERATOR_LIB + +#include "fa2_convertors.mligo" +#include "../fa2_errors.mligo" + +(* (owner, operator) -> unit *) +type operator_storage = ((address * address), unit) big_map + + +let update_operators (update, storage : update_operator * operator_storage) + : operator_storage = + match update with + | Add_operator_p op -> + Big_map.update (op.owner, op.operator) (Some unit) storage + | Remove_operator_p op -> + Big_map.remove (op.owner, op.operator) storage + +let validate_update_operators_by_owner (update, updater : update_operator * address) + : unit = + let op = match update with + | Add_operator_p op -> op + | Remove_operator_p op -> op + in + if op.owner = updater then unit else failwith not_owner + + +let is_operator (param, storage : is_operator_param * operator_storage) : operation = + let op_key = (param.operator.owner, param.operator.operator) in + let is_op = Big_map.mem op_key storage in + let r : is_operator_response = { + operator = param.operator; + is_operator = is_op; + } in + let rm = is_operator_response_to_michelson r in + Operation.transaction rm 0mutez param.callback + +let make_operator_validator (tx_policy : operator_transfer_policy) + : (address * operator_storage)-> unit = + let can_owner_tx, can_operator_tx = match tx_policy with + | No_transfer -> (failwith tx_denied : bool * bool) + | Owner_transfer -> true, false + | Owner_or_operator_transfer -> true, true + in + let operator : address = Tezos.sender in + (fun (owner, ops_storage : address * operator_storage) -> + if can_owner_tx && owner = operator + then unit + else + if not can_operator_tx + then failwith not_owner + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith not_operator + ) + +(** validate operators for all transfers in the batch at once*) +let validate_operator (tx_policy, txs, ops_storage + : operator_transfer_policy * (transfer list) * operator_storage) : unit = + let validator = make_operator_validator tx_policy in + List.iter (fun (tx : transfer) -> validator (tx.from_, ops_storage)) txs + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md new file mode 100644 index 0000000..98df4ee --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md @@ -0,0 +1,210 @@ +# Chapter 25 : Financial Asset 2.0 - Tranfer Hook + +Captain, all space pirate should have a hook like in old times. + +## ... in the previous episode + +The FA2 standard proposes a *unified token contract interface* that accommodates all mentioned concerns. It aims to provide significant expressivity to contract developers to create new types of tokens while maintaining a common interface standard for wallet integrators and external developers. + +The FA2 interface formalize a standard way to design tokens and thus describes a list of entrypoints (that must be implemented) and data structures related to those entrypoints. + +In this chapter we will focus on _transfer hook_ + +### Transfer Hook + +The FA2 standard proposes an approach in which a pluggable separate contract (permission transfer hook) is implemented and registered with the core FA2. Every time FA2 performs a transfer, it invokes a "hook" contract that validates a transaction and either approves it by finishing execution successfully or rejects it by failing. + + +#### definition + +_Transfer hook_ is one recommended design pattern to implement FA2 that enables separation of the core token transfer logic and a permission policy. + +Instead of implementing FA2 as a monolithic contract, a permission policy can be implemented as a separate contract. Permission policy contract provides an entry point invoked by the core FA2 contract to accept or reject a particular transfer operation (such an entry point is called *transfer hook*). + +Although this approach introduces gas consumption overhead (compared to an all-in-one contract) by requiring an extra inter-contract call, it also offers some other advantages: +1) FA2 core implementation can be verified once, and certain properties (not related to permission policy) remain unchanged. +2) modification of the permission policy of an existing contract can be done by replacing a transfer hook only. No storage migration of the FA2 ledger is required. +3) Transfer hooks could be used for purposes beyond permissioning, such as implementing _custom logic_ for a particular token application + +The transfer hook makes it possible to model different transfer permission policies like whitelists, operator lists, etc. + + +#### Hook interface + +The FA2 interface formalize a standard way to handle hooks. + +``` +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb + +type fa2_with_hook_entry_points = + | Fa2 of fa2_entry_points + | Set_transfer_hook of set_hook_param_michelson +``` + +In addition to the hook standard, the FA2 standard provides helper functions to manipulate data structures involved in FA2 interface. These helper function are packed in a FA2 library. (see section "FA2 standard hook library") + +#### FA2 standard hook library + +Some helpers functions has been gatthered in a hook library which help defining hooks when implementing a FA2 contract. This library contains following functions and type alias : + +The type _fa2_registry_ is a set of address + +the function *get_hook_entrypoint* retrieves the contract interface of entrypoint "%tokens_transferred_hook" for a given contract address + +the function *register_with_fa2* +* takes the address of a FA2 contract (having hooks) and register it in the registry (set of address). +* calls the *Set_transfer_hook* entrypoint of a FA2 contract + +the function *create_register_hook_op* sends a transaction to a FA2 contract (having hook entrypoints). The transaction intends to invoke the entrypoint *Set_transfer_hook*. This entrypoint *Set_transfer_hook* requires as parameters : +* the contract interface of entrypoint "%tokens_transferred_hook" +* a _permission descriptor_ + +the function *validate_hook_call* ensures an address in registered in the registry (set of address). + +#### Hook Rules + +FA2 implementation with the transfer hook pattern recquires following rules: + +1) An FA2 token contract has a single entry point to set the hook. If a transfer hook is not set, the FA2 token contract transfer operation MUST fail. + +2) Transfer hook is to be set by the token contract administrator before any transfers can happen. + +3) The concrete token contract implementation MAY impose additional restrictions on +who may set the hook. If the set hook operation is not permitted, it MUST fail +without changing existing hook configuration. + +4) For each transfer operation, a token contract MUST invoke a transfer hook and +return a corresponding operation as part of the transfer entry point result. +(For more details see set_transfer_hook ) + +5) *operator* parameter for the hook invocation MUST be set to *SENDER*. + +6) *from_* parameter for each hook_transfer batch entry MUST be set to *Some(transfer.from_)*. + +7) *to_* parameter for each hook_transfer batch entry MUST be set to *Some(transfer.to_)*. + +8) A transfer hook MUST be invoked, and operation returned by the hook invocation +MUST be returned by transfer entry point among other operations it might create. +*SENDER* MUST be passed as an operator parameter to any hook invocation. If an +invoked hook fails, the whole transfer transaction MUST fail. + +9) FA2 does NOT specify an interface for mint and burn operations; however, if an +FA2 token contract implements mint and burn operations, these operations MUST +invoke a transfer hook as well. + +#### Implementation of a custom hook + +Let's see an example of FA2 implementation. The following smart contract implements a token where transfer receiver must be in a whitelist. This whitelisting is done via a tranfer hook. +It uses a combination of a receiver white list and *fa2_token_receiver* interface. +Transfer is permitted if a receiver address is in the receiver white list OR implements *fa2_token_receiver* interface. +If a receiver address implements *fa2_token_receiver* interface, its *tokens_received* entry point must be called. + +``` +#include "../lib/fa2_hook_lib.mligo" +#include "../lib/fa2_behaviors.mligo" + + +type storage = { + fa2_registry : fa2_registry; + receiver_whitelist : address set; +} + +let custom_validate_receivers (p, wl : transfer_descriptor_param * (address set)) + : operation list = + let get_receiver : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in + let receivers = get_owners_from_batch (p.batch, get_receiver) in + + Set.fold + (fun (ops, r : (operation list) * address) -> + let hook, err = to_sender_hook r in + match hook with + | Some h -> + let pm = transfer_descriptor_param_to_michelson p in + let op = Operation.transaction pm 0mutez h in + op :: ops + | None -> + if Set.mem r wl + then ops + else (failwith err : operation list) + ) + receivers ([] : operation list) + +let custom_transfer_hook (p, s : transfer_descriptor_param * storage) : operation list = + custom_validate_receivers (p, s.receiver_whitelist) + + +let get_policy_descriptor (u : unit) : permissions_descriptor = + { + operator = Owner_or_operator_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook ; (* overridden by the custom policy *) + custom = Some { + tag = "receiver_hook_and_whitelist"; + config_api = (Some Current.self_address); + }; + } + +type config_whitelist = + | Add_receiver_to_whitelist of address set + | Remove_receiver_from_whitelist of address set + +let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) + : address set = + match cfg with + | Add_receiver_to_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.add a l) + rs wl + | Remove_receiver_from_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.remove a l) + rs wl + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_receiver_whitelist of config_whitelist + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook p -> + // verify s.fa2_registry contains p.fa2 address otherwise throw exception + let u = validate_hook_call (p.fa2, s.fa2_registry) in + let ops = custom_transfer_hook (p, s) in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = get_policy_descriptor unit in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_receiver_whitelist cfg -> + let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in + let new_s = { s with receiver_whitelist = new_wl; } in + ([] : operation list), new_s + +``` + + + +## Your mission + + + +1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. + + + diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md new file mode 100644 index 0000000..c97051d --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md @@ -0,0 +1,164 @@ +# Chapter 24 : Financial Asset 2.0 - Operators and Permissions + +Captain, why are you trying to change the part yourself? Just write a function on the terminal and send it to a droid. + +## Definition + +The FA2 standard proposes a *unified token contract interface* that accommodates all mentioned concerns. It aims to provide significant expressivity to contract developers to create new types of tokens while maintaining a common interface standard for wallet integrators and external developers. + +In this chapter we will focus on _Operators_ and _Permissions_. + + +### Operators + +#### Definition + +_Operator_ is a Tezos address that initiates token transfer operation on behalf of the owner. +_Owner_ is a Tezos address which can hold tokens. +An operator, other than the owner, MUST be approved to manage particular token types held by the owner to make a transfer from the owner account. +Operator relation is not transitive. If C is an operator of B , and if B is an operator of A, C cannot transfer tokens that are owned by A, on behalf of B. + +an _operator_ is defined as a relationship between two address (owner address and operator address) and can be understood as an operator address who can operate tokens held by a owner. + +#### FA2 interface operator + +FA2 interface specifies two entry points to update and inspect operators. Once permitted for the specific token owner, an operator can transfer any tokens belonging to the owner. + +``` +| Update_operators of update_operator list +| Is_operator of is_operator_param +``` + +where parameter type *update_operator* and *is_operator_param* are : +``` +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type is_operator_param = { + operator : operator_param; + callback : (is_operator_response_michelson) contract; +} +``` + +Notice the *update_operator* can only Add or Remove an _operator_ (an allowance between an operator address and a token owner address). + +Notice the parameter _is_operator_param_ given to *Is_operator* entry point contains a *callback* property used to send back a response to the calling contract. + +#### FA2 standard operator library + +Some helpers functions has been implemented in the FA2 library which help manipulating _operator_. This library contains following functions and type alias : + + +an _operator_ is a relationship between two address (owner address and operator address) + +function *is_operator* returns to a contract caller whether an operator address is associated to an owner address + +function *update_operators* allows to Add or Remove an operator in the list of operators. + +function *validate_update_operators_by_owner*, it ensures the given adress is owner of an _operator_ + +the function *validate_operator* validates operators for all transfers in the batch at once, depending on given operator_transfer_policy + + + +### FA2 Permission Policies and Configuration + +Most token standards specify logic that validates a transfer transaction and can either approve or reject a transfer. +Such logic (called _Permission Policy_) could validate who initiates a transfer, the transfer amount, and who can receive tokens. + +This FA2 standard defines a framework to compose and configure such permission policies from the standard behaviors and configuration APIs. + +FA2 defines : +* the default core transfer behavior, that MUST always be implemented +* a set of predefined permission policies that are optional + + +#### permissions_descriptor + +FA2 specifies an interface permissions_descriptor allowing external contracts to discover an FA2 contract's permission policy and to configure it. *permissions_descriptor* serves as a modular approach to define consistent and non-self-contradictory policies. + +The *permission descriptor* indicates which standard permission policies are implemented by the FA2 contract and can be used by off-chain and on-chain tools to discover the properties of the particular FA2 contract implementation. + +The FA2 standard defines a special metadata entry point *permission descriptor* containing standard permission policies. +``` +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} +``` + + +#### operator_transfer_policy + +operator_transfer_policy - defines who can transfer tokens. Tokens can be +transferred by the token owner or an operator (some address that is authorized to +transfer tokens on behalf of the token owner). A special case is when neither owner +nor operator can transfer tokens (can be used for non-transferable tokens). The +FA2 standard defines two entry points to manage and inspect operators associated +with the token owner address (*update_operators*, +*is_operator*). Once an operator is added, it can manage all of +its associated owner's tokens. + +``` +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer +``` + +#### owner_hook_policy + +owner_hook_policy - defines if sender/receiver hooks should be called or +not. Each token owner contract MAY implement either an *fa2_token_sender* or +*fa2_token_receiver* hook interface. Those hooks MAY be called when a transfer sends +tokens from the owner account or the owner receives tokens. The hook can either +accept a transfer transaction or reject it by failing. + +``` +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook +``` + +#### custom_permission_policy + +It is possible to extend permission policy with a custom behavior, which does +not overlap with already existing standard policies. This standard does not specify +exact types for custom config entry points. FA2 token contract clients that support +custom config entry points must know their types a priori and/or use a tag hint +of custom_permission_policy. + +``` +type custom_permission_policy = { + tag : string; + config_api: address option; +} +``` + + +#### Permission Policy Formulae + +Each concrete implementation of the permission policy can be described by a formula which combines permission behaviors in the following form: +``` +Operator(?) * Receiver(?) * Sender(?) +``` + +This formula describes the policy which allows only token owners to transfer their own +tokens : +``` +Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) +``` + + + + + +## Your mission + + +1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. + From 1790a5a075e67649b7d462a9ecc02f0a400ff08a Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Thu, 11 Jun 2020 11:03:27 +0200 Subject: [PATCH 10/16] [cameligo] FA2 part1 exercise + fungible impl --- .../Chapters/Camel/ChapterFA20/caller.mligo | 31 ++- .../Chapters/Camel/ChapterFA20/course.md | 60 +++--- .../Chapters/Camel/ChapterFA20/exercise.mligo | 97 ++++++--- .../Camel/ChapterFA20/fungible_token.mligo | 194 ++++++++++++++++++ .../pages/Chapters/Camel/ChapterFA20/index.ts | 17 ++ .../Chapters/Camel/ChapterFA20/solution.mligo | 193 +++++++++++++++++ .../Camel/ChapterFA20Permission/course.md | 30 ++- .../Camel/ChapterFA20Permission/exercise.cmd | 5 + 8 files changed, 558 insertions(+), 69 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/fungible_token.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo index a4d6843..e04f9b9 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/caller.mligo @@ -2,6 +2,8 @@ #include "tzip/proposals/tzip-12/lib/fa2_behaviors.mligo" type storage = { + fa2_registry : fa2_registry; + descriptor : permissions_descriptor; rep : balance_of_response_michelson list } @@ -14,10 +16,10 @@ type receive_balance_of_param = balance_of_response_michelson list let request_balance (req, s : (request_balance_of_param * storage)) : operation list * storage = // Get the TZIP-12 contract instance 'from' the chain - let token_contract_balance_entrypoint_opt : balance_of_param contract option = Tezos.get_entrypoint_opt "%balance_of" req.at in - let token_contract_balance_entrypoint : balance_of_param contract = match (token_contract_balance_entrypoint_opt) with + let token_contract_balance_entrypoint_opt : balance_of_param_michelson contract option = Tezos.get_entrypoint_opt "%balance_of" req.at in + let token_contract_balance_entrypoint : balance_of_param_michelson contract = match (token_contract_balance_entrypoint_opt) with | Some (ci) -> ci - | None -> (failwith("Entrypoint not found"): balance_of_param contract) + | None -> (failwith("Entrypoint not found"): balance_of_param_michelson contract) in // Callback (contract) for Balance_of will be the current contract's Receive_balance entrypoint @@ -28,14 +30,17 @@ let request_balance (req, s : (request_balance_of_param * storage)) : operation in //let balance_of_callback_contract : receive_balance_of_param contract = get_entrypoint("%receive_balance", Tezos.self_address) in // Set up the parameter w/ data required for the Balance_of entrypoint - let balance_of_operation_param : balance_of_param = { - requests = req.requests; + let convert = fun (i : balance_of_request) -> Layout.convert_to_right_comb(i) in + let request_michelson : balance_of_request_michelson list = List.map convert req.requests in + let balance_of_operation_param : balance_of_param_aux = { + requests = request_michelson; callback = balance_of_callback_contract; } in + let reqPayload : balance_of_param_michelson = Layout.convert_to_right_comb(balance_of_operation_param) in // Forge an internal transaction to the TZIP-12 contract // parametrised by the prieviously prepared `balance_of_operation_param` // Note: We're sending 0mutez as part of this transaction - let balance_of_operation : operation = Tezos.transaction balance_of_operation_param 0mutez token_contract_balance_entrypoint in + let balance_of_operation : operation = Tezos.transaction reqPayload 0mutez token_contract_balance_entrypoint in ([ balance_of_operation ], s) let receive_balance (received, s: (receive_balance_of_param * storage)) : operation list * storage = @@ -45,9 +50,23 @@ let receive_balance (received, s: (receive_balance_of_param * storage)) : operat type entry_points = | Request_balance of request_balance_of_param | Receive_balance of receive_balance_of_param + | Tokens_transferred_hook of transfer_descriptor_param_michelson + | Register_with_fa2 of fa2_with_hook_entry_points contract let main (param, s : entry_points * storage) : (operation list) * storage = match param with + | Tokens_transferred_hook pm -> + let p = transfer_descriptor_param_from_michelson pm in + let u = validate_hook_call (p.fa2, s.fa2_registry) in + let ops = standard_transfer_hook ( + {ligo_param = p; michelson_param = pm}, s.descriptor) in + ops, s + + | Register_with_fa2 fa2 -> + let op , new_registry = register_with_fa2 (fa2, s.descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + | Request_balance request_balance_of_param -> request_balance (request_balance_of_param, s) | Receive_balance receive_balance_of_param -> receive_balance (receive_balance_of_param, s) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md index 61a809e..79e4c63 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/course.md @@ -12,6 +12,16 @@ The FA2 standard proposes a *unified token contract interface* that accommodates In the following chapter on Financial Asset 2.0 , we will focus on *TZIP-12* which stands for the 12th Tezos Improvement Proposal (same as EIP-721 for Ethereum). +## Architecture + +FA2 proposes to leave it to implementers to handle common considerations such as defining the contract’s token type(s) (e.g. non-fungible vs. fungible vs. semi-fungible), administration and whitelisting, contract upgradability, and supply operations (e.g. mint/burn). + +FA2 also leaves to implementers to decide on architecture pattern for handling permissioning. Permission can be implemented +* in the the same contract as the core transfer behavior (i.e. a “monolith”), +* in a transfer hook to another contract, +* in a separate wrapper contract. + + ## Interface and library The FA2 interface formalize a standard way to design tokens and thus describes a list of entry points (that must be implemented) and data structures related to those entry points. A more detailed decription of the interface is broken down in following sections. @@ -184,8 +194,6 @@ type transfer_aux = { from_ : address; txs : transfer_destination_michelson list; } - - ``` ### Error Handling @@ -203,53 +211,37 @@ When error occurs, any FA2 contract entry point MUST fail with one of the follow #### Standard error mnemonics: -Error mnemonic -Description - - - +Error mnemonic - Description -"TOKEN_UNDEFINED" -One of the specified token_ids is not defined within the FA2 contract +"TOKEN_UNDEFINED" - One of the specified token_ids is not defined within the FA2 contract +"INSUFFICIENT_BALANCE" - A token owner does not have sufficient balance to transfer tokens from owner's account -"INSUFFICIENT_BALANCE" -A token owner does not have sufficient balance to transfer tokens from owner's account +"TX_DENIED" - A transfer failed because of operator_transfer_policy == No_transfer +"NOT_OWNER" - A transfer failed because operator_transfer_policy == Owner_transfer and it is initiated not by the token owner -"TX_DENIED" -A transfer failed because of operator_transfer_policy == No_transfer +"NOT_OPERATOR" - A transfer failed because operator_transfer_policy == Owner_or_operator_transfer and it is initiated neither by the token owner nor a permitted operator +"RECEIVER_HOOK_FAILED" - The receiver hook failed. This error MUST be raised by the hook implementation +"SENDER_HOOK_FAILED" - The sender failed. This error MUST be raised by the hook implementation -"NOT_OWNER" -A transfer failed because operator_transfer_policy == Owner_transfer and it is initiated not by the token owner +"RECEIVER_HOOK_UNDEFINED" -Receiver hook is required by the permission behavior, but is not implemented by a receiver contract +"SENDER_HOOK_UNDEFINED" - Sender hook is required by the permission behavior, but is not implemented by a sender contract -"NOT_OPERATOR" -A transfer failed because operator_transfer_policy == Owner_or_operator_transfer and it is initiated neither by the token owner nor a permitted operator -"RECEIVER_HOOK_FAILED" -The receiver hook failed. This error MUST be raised by the hook implementation - - -"SENDER_HOOK_FAILED" -The sender failed. This error MUST be raised by the hook implementation - - -"RECEIVER_HOOK_UNDEFINED" -Receiver hook is required by the permission behavior, but is not implemented by a receiver contract - - -"SENDER_HOOK_UNDEFINED" -Sender hook is required by the permission behavior, but is not implemented by a sender contract - +## Your mission +We are working on a fungible/multi-asset token compliant with the FA2 standard. We want you to complete the existing implementation of token. The Total_supply entry point is not yet implemented , please finish the job ! +1 - Modify the *get_total_supply* lambda function in order to retrieve the total_supply information related to the given *token_id* list. -## Your mission +2 - the *get_total_supply* lambda function For each given token_id, find the given *token_id* in the *tokens* map and retrieve the *total_supply* associated to a given *token_id* in the *tokens* map. +3 -If a given token_id is found then the function *get_total_supply* must return a *total_supply_response* record for each given *token_id*. As seen in the interface the *total_supply_response* record contains *token_id* and *total_supply* fields. (use v as temporary variable for the match with instruction) -1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. +4 -If a given token_id is not found then the function *get_total_supply* must throw an exception with the predefined error messsage *token_undefined*. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo index fca09fd..9b61bd0 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo @@ -12,17 +12,13 @@ type tokens = { metadata : token_metadata; } -type operator_ = { - owner : address; - operator : address; -} - type storage = { paused : bool; ledger : ledger; - tokens : (nat,tokens) map; - operators : operator_ set; + tokens : (token_id,tokens) map; + operators : operator_param set; administrator : address; + permissions_descriptor : permissions_descriptor_aux; } type return = (operation list * storage) @@ -30,10 +26,8 @@ type return = (operation list * storage) type entry_points = | Set_pause of bool | Set_administrator of address - | MyBalance_of of balance_of_param - | MyTotal_supply of total_supply_param_michelson - | MyToken_metadata of token_metadata_param_michelson - | MyTransfer of transfer_michelson list + +type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or let set_pause(param,s : bool * storage): return = if Tezos.sender = s.administrator then @@ -47,16 +41,19 @@ let set_administrator(param,s : address * storage): return = else (failwith("only admin can do it") : return) -let balance_of (balance_of_param, s : balance_of_param * storage) : return = - let get_balance = fun ( i : balance_of_request) -> match Map.find_opt (i.owner,i.token_id) s.ledger with - | Some e -> { request = i ; balance =e.balance } - | None -> (failwith("unknown owner") : balance_of_response) +let balance_of (param, s : balance_of_param_michelson * storage) : return = + let param_bo_aux : balance_of_param_aux = Layout.convert_from_right_comb(param: balance_of_param_michelson) in + let get_balance = fun ( i : balance_of_request_michelson) -> + let bor : balance_of_request = Layout.convert_from_right_comb(i) in + match Map.find_opt (bor.owner,bor.token_id) s.ledger with + | Some e -> { request = Layout.convert_to_right_comb(bor) ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response_aux) in - let balance_of_callback_param : balance_of_response list = List.map get_balance balance_of_param.requests in - let convert = fun ( r : balance_of_response) -> Layout.convert_to_right_comb(({request=Layout.convert_to_right_comb( r.request ); balance=r.balance} : balance_of_response_aux)) in + let balance_of_callback_param : balance_of_response_aux list = List.map get_balance param_bo_aux.requests in + let convert = fun ( r : balance_of_response_aux) -> Layout.convert_to_right_comb(r) in let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in // sending back the processed map of balance requests/responses - let destination: (balance_of_response_michelson list) contract = balance_of_param.callback in + let destination: (balance_of_response_michelson list) contract = param_bo_aux.callback in let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in ([balance_of_response_operation], s) @@ -66,9 +63,10 @@ let total_supply(params, s: total_supply_param_michelson * storage) : return = else let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in let token_ids : token_id list = p.token_ids in + // Modify the code below let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with | Some v -> { token_id = i ; total_supply =v.total_supply } - | None -> (failwith("unknown token_id") : total_supply_response) + | None -> (failwith(token_undefined) : total_supply_response) in let responses : total_supply_response list = List.map get_total_supply token_ids in let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in @@ -85,7 +83,7 @@ let token_metadata(params, s: token_metadata_param_michelson * storage) : return let token_ids : token_id list = p.token_ids in let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with | Some v -> v.metadata - | None -> (failwith("unknown token_id") : token_metadata) + | None -> (failwith(token_undefined) : token_metadata) in let responses : token_metadata list = List.map get_metadata token_ids in let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in @@ -141,14 +139,57 @@ let transfer(params, s: transfer_michelson list * storage) : return = result_ledger in let new_ledger : ledger = List.fold apply_transfer params s.ledger in - (([] : operation list), new_ledger) + (([] : operation list), {s with ledger=new_ledger}) + +let update_operators (params,s : (update_operator_michelson list * storage)) : return = + if Tezos.sender <> s.administrator then + (failwith("operators can only be modified by the admin") : return) + else + let convert = fun (i : update_operator_michelson) -> (Layout.convert_from_right_comb(i) : update_operator_aux) in + let params_aux_list : update_operator_aux list = List.map convert params in + let apply_order = fun (acc,j : operator_param set * update_operator_aux) -> + match j with + | Add_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.add p acc + else + (failwith("notautorized !!!! ") : operator_param set) + | Remove_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.remove p acc + else + (failwith("notautorized !!!! ") : operator_param set) + in + let new_operators : operator_param set = List.fold apply_order params_aux_list s.operators in + (([] : operation list), {s with operators=new_operators}) + +let is_operator(params,s : (is_operator_param_michelson * storage)) : return = + let p : is_operator_param_aux = Layout.convert_from_right_comb(params) in + let op_param : operator_param = Layout.convert_from_right_comb(p.operator) in + let response_aux : is_operator_response_aux = {operator=p.operator;is_operator=Set.mem op_param s.operators} in + let response : is_operator_response_michelson = Layout.convert_to_right_comb(response_aux) in + let destination: (is_operator_response_michelson) contract = p.callback in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) +let send_permissions_descriptor(param,s : (permissions_descriptor_michelson contract * storage)) : return = + let response : permissions_descriptor_michelson = Layout.convert_to_right_comb(s.permissions_descriptor) in + let destination: permissions_descriptor_michelson contract = param in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) -let main (p,s : entry_points * storage) : return = - match p with - | MyTotal_supply p -> total_supply (p,s) - | MyBalance_of p -> balance_of (p,s) - | MyToken_metadata p -> token_metadata (p,s) +let main (param,s : total_entry_points * storage) : return = + match param with + | M_left fa2_ep -> (match fa2_ep with + | Transfer l -> transfer (l, s) + | Balance_of p -> balance_of (p, s) + | Total_supply p -> total_supply (p,s) + | Token_metadata p -> token_metadata (p,s) + | Permissions_descriptor callback -> send_permissions_descriptor (callback, s) + | Update_operators l -> update_operators (l,s) + | Is_operator o -> is_operator (o,s)) + | M_right specific_ep -> (match specific_ep with | Set_pause p -> set_pause (p,s) - | Set_administrator p -> set_administrator (p,s) - | MyTransfer l -> transfer (l, s) \ No newline at end of file + | Set_administrator p -> set_administrator (p,s)) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/fungible_token.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/fungible_token.mligo new file mode 100644 index 0000000..2bfbe83 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/fungible_token.mligo @@ -0,0 +1,194 @@ +#include "tzip/proposals/tzip-12/fa2_interface.mligo" +#include "tzip/proposals/tzip-12/fa2_errors.mligo" + +type entity = { + balance : nat +} +type entity_key = address * token_id +type ledger = (entity_key, entity) map + +type tokens = { + total_supply : nat; + metadata : token_metadata; +} + +type storage = { + paused : bool; + ledger : ledger; + tokens : (token_id,tokens) map; + operators : operator_param set; + administrator : address; + permissions_descriptor : permissions_descriptor_aux; +} + +type return = (operation list * storage) + +type entry_points = + | Set_pause of bool + | Set_administrator of address + +type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or + +let set_pause(param,s : bool * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with paused=param }) + else + (failwith("only admin can do it") : return) + +let set_administrator(param,s : address * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with administrator=param }) + else + (failwith("only admin can do it") : return) + +let balance_of (param, s : balance_of_param_michelson * storage) : return = + let param_bo_aux : balance_of_param_aux = Layout.convert_from_right_comb(param: balance_of_param_michelson) in + let get_balance = fun ( i : balance_of_request_michelson) -> + let bor : balance_of_request = Layout.convert_from_right_comb(i) in + match Map.find_opt (bor.owner,bor.token_id) s.ledger with + | Some e -> { request = Layout.convert_to_right_comb(bor) ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response_aux) + in + let balance_of_callback_param : balance_of_response_aux list = List.map get_balance param_bo_aux.requests in + let convert = fun ( r : balance_of_response_aux) -> Layout.convert_to_right_comb(r) in + let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in + // sending back the processed map of balance requests/responses + let destination: (balance_of_response_michelson list) contract = param_bo_aux.callback in + let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in + ([balance_of_response_operation], s) + +let total_supply(params, s: total_supply_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith(token_undefined) : total_supply_response) + in + let responses : total_supply_response list = List.map get_total_supply token_ids in + let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in + let ret : total_supply_response_michelson list = List.map convert responses in + let destination: (total_supply_response_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let token_metadata(params, s: token_metadata_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : token_metadata_param = Layout.convert_from_right_comb(params: token_metadata_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> v.metadata + | None -> (failwith("unknown token_id") : token_metadata) + in + let responses : token_metadata list = List.map get_metadata token_ids in + let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in + let ret : token_metadata_michelson list = List.map convert responses in + let destination: (token_metadata_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let transfer(params, s: transfer_michelson list * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let apply_transfer = fun (l,i : ledger * transfer_michelson) -> + let t_aux : transfer_aux = Layout.convert_from_right_comb(i: transfer_michelson) in + let from_ : address = t_aux.from_ in + let result_ledger : ledger = if Tezos.sender = from_ or Tezos.sender = s.administrator then + if Set.mem {owner=from_;operator=Tezos.sender} s.operators then + let transfers : transfer_destination_michelson list = t_aux.txs in + let apply_transfer_destination = fun (acc,j : (ledger * transfer_destination_michelson)) -> + let transfer_destination : transfer_destination = Layout.convert_from_right_comb(j: transfer_destination_michelson) in + let tr_amount : nat = transfer_destination.amount in + let to_ : address = transfer_destination.to_ in + let tokenid : token_id = transfer_destination.token_id in + let temp_state_ledger : ledger = if tr_amount > 0n then + let enough_funds : bool = match Map.find_opt (from_,tokenid) acc with + | Some bal -> (bal.balance >= tr_amount) + | None -> false + in + if enough_funds then + let l_updated_from : ledger = match Map.find_opt (from_,tokenid) acc with + | Some bal -> Map.update (from_,tokenid) (Some {balance=abs(bal.balance - tr_amount)} ) acc + | None -> (failwith("should not arrive here") : ledger) + in + let l_updated_from_to : ledger = match Map.find_opt (to_,tokenid) l_updated_from with + | Some bal -> Map.update (to_,tokenid) (Some {balance=bal.balance + tr_amount}) l_updated_from + | None -> Map.add (to_,tokenid) {balance=tr_amount} l_updated_from + in + l_updated_from_to + else + (failwith(insufficient_balance) : ledger) + else + (failwith("transferring nothing !") : ledger) + in + temp_state_ledger + in + let result : ledger = List.fold apply_transfer_destination transfers l in + result + else + (failwith(not_operator) : ledger) + else + (failwith(not_owner) : ledger) + in + result_ledger + in + let new_ledger : ledger = List.fold apply_transfer params s.ledger in + (([] : operation list), {s with ledger=new_ledger}) + +let update_operators (params,s : (update_operator_michelson list * storage)) : return = + if Tezos.sender <> s.administrator then + (failwith("operators can only be modified by the admin") : return) + else + let convert = fun (i : update_operator_michelson) -> (Layout.convert_from_right_comb(i) : update_operator_aux) in + let params_aux_list : update_operator_aux list = List.map convert params in + let apply_order = fun (acc,j : operator_param set * update_operator_aux) -> + match j with + | Add_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.add p acc + else + (failwith("notautorized !!!! ") : operator_param set) + | Remove_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.remove p acc + else + (failwith("notautorized !!!! ") : operator_param set) + in + let new_operators : operator_param set = List.fold apply_order params_aux_list s.operators in + (([] : operation list), {s with operators=new_operators}) + +let is_operator(params,s : (is_operator_param_michelson * storage)) : return = + let p : is_operator_param_aux = Layout.convert_from_right_comb(params) in + let op_param : operator_param = Layout.convert_from_right_comb(p.operator) in + let response_aux : is_operator_response_aux = {operator=p.operator;is_operator=Set.mem op_param s.operators} in + let response : is_operator_response_michelson = Layout.convert_to_right_comb(response_aux) in + let destination: (is_operator_response_michelson) contract = p.callback in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let send_permissions_descriptor(param,s : (permissions_descriptor_michelson contract * storage)) : return = + let response : permissions_descriptor_michelson = Layout.convert_to_right_comb(s.permissions_descriptor) in + let destination: permissions_descriptor_michelson contract = param in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let main (param,s : total_entry_points * storage) : return = + match param with + | M_left fa2_ep -> (match fa2_ep with + | Transfer l -> transfer (l, s) + | Balance_of p -> balance_of (p, s) + | Total_supply p -> total_supply (p,s) + | Token_metadata p -> token_metadata (p,s) + | Permissions_descriptor callback -> send_permissions_descriptor (callback, s) + | Update_operators l -> update_operators (l,s) + | Is_operator o -> is_operator (o,s)) + | M_right specific_ep -> (match specific_ep with + | Set_pause p -> set_pause (p,s) + | Set_administrator p -> set_administrator (p,s)) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts new file mode 100644 index 0000000..504cda9 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/index.ts @@ -0,0 +1,17 @@ +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import course from "!raw-loader!./course.md"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import exercise from "!raw-loader!./exercise.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import solution from "!raw-loader!./solution.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support1 from "!raw-loader!./fungible_token.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support2 from "!raw-loader!./caller.mligo"; + +export const data = { course, exercise, solution, support1, support2 }; diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo new file mode 100644 index 0000000..5f3484f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo @@ -0,0 +1,193 @@ +#include "tzip/proposals/tzip-12/fa2_interface.mligo" +#include "tzip/proposals/tzip-12/fa2_errors.mligo" + +type entity = { + balance : nat +} +type entity_key = address * token_id +type ledger = (entity_key, entity) map + +type tokens = { + total_supply : nat; + metadata : token_metadata; +} + +type storage = { + paused : bool; + ledger : ledger; + tokens : (token_id,tokens) map; + operators : operator_param set; + administrator : address; + permissions_descriptor : permissions_descriptor_aux; +} + +type return = (operation list * storage) + +type entry_points = + | Set_pause of bool + | Set_administrator of address + +type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or + +let set_pause(param,s : bool * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with paused=param }) + else + (failwith("only admin can do it") : return) + +let set_administrator(param,s : address * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with administrator=param }) + else + (failwith("only admin can do it") : return) + +let balance_of (param, s : balance_of_param_michelson * storage) : return = + let param_bo_aux : balance_of_param_aux = Layout.convert_from_right_comb(param: balance_of_param_michelson) in + let get_balance = fun ( i : balance_of_request_michelson) -> + let bor : balance_of_request = Layout.convert_from_right_comb(i) in + match Map.find_opt (bor.owner,bor.token_id) s.ledger with + | Some e -> { request = Layout.convert_to_right_comb(bor) ; balance =e.balance } + | None -> (failwith("unknown owner") : balance_of_response_aux) + in + let balance_of_callback_param : balance_of_response_aux list = List.map get_balance param_bo_aux.requests in + let convert = fun ( r : balance_of_response_aux) -> Layout.convert_to_right_comb(r) in + let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in + // sending back the processed map of balance requests/responses + let destination: (balance_of_response_michelson list) contract = param_bo_aux.callback in + let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in + ([balance_of_response_operation], s) + +let total_supply(params, s: total_supply_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in + let token_ids : token_id list = p.token_ids in + // Modify the code below + let get_total_supply = + in + let responses : total_supply_response list = List.map get_total_supply token_ids in + let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in + let ret : total_supply_response_michelson list = List.map convert responses in + let destination: (total_supply_response_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let token_metadata(params, s: token_metadata_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : token_metadata_param = Layout.convert_from_right_comb(params: token_metadata_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> v.metadata + | None -> (failwith(token_undefined) : token_metadata) + in + let responses : token_metadata list = List.map get_metadata token_ids in + let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in + let ret : token_metadata_michelson list = List.map convert responses in + let destination: (token_metadata_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let transfer(params, s: transfer_michelson list * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let apply_transfer = fun (l,i : ledger * transfer_michelson) -> + let t_aux : transfer_aux = Layout.convert_from_right_comb(i: transfer_michelson) in + let from_ : address = t_aux.from_ in + let result_ledger : ledger = if Tezos.sender = from_ or Tezos.sender = s.administrator then + if Set.mem {owner=from_;operator=Tezos.sender} s.operators then + let transfers : transfer_destination_michelson list = t_aux.txs in + let apply_transfer_destination = fun (acc,j : (ledger * transfer_destination_michelson)) -> + let transfer_destination : transfer_destination = Layout.convert_from_right_comb(j: transfer_destination_michelson) in + let tr_amount : nat = transfer_destination.amount in + let to_ : address = transfer_destination.to_ in + let tokenid : token_id = transfer_destination.token_id in + let temp_state_ledger : ledger = if tr_amount > 0n then + let enough_funds : bool = match Map.find_opt (from_,tokenid) acc with + | Some bal -> (bal.balance >= tr_amount) + | None -> false + in + if enough_funds then + let l_updated_from : ledger = match Map.find_opt (from_,tokenid) acc with + | Some bal -> Map.update (from_,tokenid) (Some {balance=abs(bal.balance - tr_amount)} ) acc + | None -> (failwith("should not arrive here") : ledger) + in + let l_updated_from_to : ledger = match Map.find_opt (to_,tokenid) l_updated_from with + | Some bal -> Map.update (to_,tokenid) (Some {balance=bal.balance + tr_amount}) l_updated_from + | None -> Map.add (to_,tokenid) {balance=tr_amount} l_updated_from + in + l_updated_from_to + else + (failwith(insufficient_balance) : ledger) + else + (failwith("transferring nothing !") : ledger) + in + temp_state_ledger + in + let result : ledger = List.fold apply_transfer_destination transfers l in + result + else + (failwith(not_operator) : ledger) + else + (failwith(not_owner) : ledger) + in + result_ledger + in + let new_ledger : ledger = List.fold apply_transfer params s.ledger in + (([] : operation list), {s with ledger=new_ledger}) + +let update_operators (params,s : (update_operator_michelson list * storage)) : return = + if Tezos.sender <> s.administrator then + (failwith("operators can only be modified by the admin") : return) + else + let convert = fun (i : update_operator_michelson) -> (Layout.convert_from_right_comb(i) : update_operator_aux) in + let params_aux_list : update_operator_aux list = List.map convert params in + let apply_order = fun (acc,j : operator_param set * update_operator_aux) -> + match j with + | Add_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.add p acc + else + (failwith("notautorized !!!! ") : operator_param set) + | Remove_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.remove p acc + else + (failwith("notautorized !!!! ") : operator_param set) + in + let new_operators : operator_param set = List.fold apply_order params_aux_list s.operators in + (([] : operation list), {s with operators=new_operators}) + +let is_operator(params,s : (is_operator_param_michelson * storage)) : return = + let p : is_operator_param_aux = Layout.convert_from_right_comb(params) in + let op_param : operator_param = Layout.convert_from_right_comb(p.operator) in + let response_aux : is_operator_response_aux = {operator=p.operator;is_operator=Set.mem op_param s.operators} in + let response : is_operator_response_michelson = Layout.convert_to_right_comb(response_aux) in + let destination: (is_operator_response_michelson) contract = p.callback in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let send_permissions_descriptor(param,s : (permissions_descriptor_michelson contract * storage)) : return = + let response : permissions_descriptor_michelson = Layout.convert_to_right_comb(s.permissions_descriptor) in + let destination: permissions_descriptor_michelson contract = param in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let main (param,s : total_entry_points * storage) : return = + match param with + | M_left fa2_ep -> (match fa2_ep with + | Transfer l -> transfer (l, s) + | Balance_of p -> balance_of (p, s) + | Total_supply p -> total_supply (p,s) + | Token_metadata p -> token_metadata (p,s) + | Permissions_descriptor callback -> send_permissions_descriptor (callback, s) + | Update_operators l -> update_operators (l,s) + | Is_operator o -> is_operator (o,s)) + | M_right specific_ep -> (match specific_ep with + | Set_pause p -> set_pause (p,s) + | Set_administrator p -> set_administrator (p,s)) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md index c97051d..271a062 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md @@ -8,10 +8,26 @@ The FA2 standard proposes a *unified token contract interface* that accommodates In this chapter we will focus on _Operators_ and _Permissions_. +## Entry points + +Token contract implementing the FA2 standard MUST have the following entry points. + +``` +type fa2_entry_points = + +| Transfer of transfer list +| Balance_of of balance_of_param +| Total_supply of total_supply_param +| Token_metadata of token_metadata_param +| Permissions_descriptor of permissions_descriptor contract +| Update_operators of update_operator list +| Is_operator of is_operator_param +``` ### Operators #### Definition +_Operator_ can be seen as delegate role. _Operator_ is a Tezos address that initiates token transfer operation on behalf of the owner. _Owner_ is a Tezos address which can hold tokens. @@ -160,5 +176,17 @@ Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) ## Your mission -1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. +Our NFT "token" is almost ready but to allow a new rule. We need A to transfert a token taken from B account and send it to C account. + +1- First we have to set the right operator policy to authorize delegation when deploying the contract. We want you to prepare the initial state of storage. Write the _ligo compile-storage_ command for the *token* contract with following recommandations : + * operator transfer is authorized, + * A account has a balance set to 100 + * B account has a balance set to 100 + * C account has a balance set to 100 + * D is the administrator of the contract + + +2- Write the _ligo dry-run_ command for authorizing A to transfer token taken from B account, transaction emitted by D. (reuse the storage you made on step 1) + +3- Write the _ligo dry-run_ command for simulating the transfer of 1 mutez from B'account to c's account, transaction emitted by A. You will have to modify the storage to take step 2 into account. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd new file mode 100644 index 0000000..c9b5de0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd @@ -0,0 +1,5 @@ +//let new_perm : permissions_descriptor = {operator=Owner_or_operator_transfer; receiver=Owner_no_hook; sender=Owner_no_hook; custom=(None : custom_permission_policy option) } + +ligo compile-storage shiptoken.mligo main '' +ligo dry-run shiptoken.ligo 'Update_operators([{owner=TZ1;operator=TZ1}])' '' +ligo dry-run --sender=A shiptoken.ligo 'Transfer(B,C,token_id)' \ No newline at end of file From a64396057efdee16b4e50cb8c47a503535ea04b0 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Fri, 12 Jun 2020 18:39:28 +0200 Subject: [PATCH 11/16] [Cameligo] FA2_chapter: implementation of a non-fungible-token --- .../Camel/ChapterFA20Permission/course.md | 2 +- .../Camel/ChapterFA20Permission/exercise.cmd | 4 +- .../non_fungible_token.mligo | 255 ++++++++++++++++++ .../Chapters/Camel/ChapterLambda/course.md | 13 +- .../Chapters/Camel/ChapterMultisig/course.md | 2 +- .../Chapters/Pascal/ChapterLambda/course.md | 14 +- .../Chapters/Pascal/ChapterMultisig/course.md | 2 +- .../Chapters/Reason/ChapterLambda/course.md | 14 +- .../Chapters/Reason/ChapterMultisig/course.md | 2 +- 9 files changed, 277 insertions(+), 31 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md index 271a062..8c96355 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md @@ -175,7 +175,7 @@ Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) ## Your mission - +We are working on a non_fungible/multi-asset token. Our NFT "token" is almost ready but to allow a new rule. We need A to transfert a token taken from B account and send it to C account. 1- First we have to set the right operator policy to authorize delegation when deploying the contract. We want you to prepare the initial state of storage. Write the _ligo compile-storage_ command for the *token* contract with following recommandations : diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd index c9b5de0..bb78a16 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd @@ -1,5 +1,3 @@ -//let new_perm : permissions_descriptor = {operator=Owner_or_operator_transfer; receiver=Owner_no_hook; sender=Owner_no_hook; custom=(None : custom_permission_policy option) } - -ligo compile-storage shiptoken.mligo main '' +ligo compile-storage non_fungible_token.mligo main '' ligo dry-run shiptoken.ligo 'Update_operators([{owner=TZ1;operator=TZ1}])' '' ligo dry-run --sender=A shiptoken.ligo 'Transfer(B,C,token_id)' \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo new file mode 100644 index 0000000..e845d6a --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo @@ -0,0 +1,255 @@ +#include "tzip/proposals/tzip-12/fa2_interface.mligo" +#include "tzip/proposals/tzip-12/fa2_errors.mligo" + +type entity = { + code : string; + name : string; +} +type entity_id = nat +type entity_key = (entity_id * token_id) +type entities = (entity_key, entity) map +type entityIndexToOwner = (entity_key, address) map +type owner_entities = (address, entity_key set) map +//type owner_entities_count = (address, nat) map + +type token = { + total_supply : nat; + metadata : token_metadata; +} + +type storage = { + paused : bool; + entities : entities; + entity_owner : entityIndexToOwner; + owner_entities : owner_entities; + tokens : (token_id,token) map; + operators : operator_param set; + administrator : address; + permissions_descriptor : permissions_descriptor_aux; +} + +type return = (operation list * storage) + +type entry_points = + | Set_pause of bool + | Set_administrator of address + | Mint of (entity * address * token_id) + +type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or + +let set_pause(param,s : bool * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with paused=param }) + else + (failwith("only admin can do it") : return) + +let set_administrator(param,s : address * storage): return = + if Tezos.sender = s.administrator then + (([] : operation list), { s with administrator=param }) + else + (failwith("only admin can do it") : return) + +let mint (param,s : (entity * address * token_id) * storage) : return = + if Tezos.sender = s.administrator then + let new_ent : entity = param.0 in + let owner : address = param.1 in + let tokenid : token_id = param.2 in + // NEVER burn an entity or entity_id will be provided an existing id (mint burn mint) + let newid : entity_id = match Map.find_opt tokenid s.tokens with + | Some tok -> tok.total_supply + 1n + | None -> (failwith("unknown token_id") : nat) + in + // entities[newid] = new_ent + let new_entities : entities = match Map.find_opt (newid,tokenid) s.entities with + | Some v -> (failwith("token already exist") : entities) + | None -> Map.add (newid,tokenid) new_ent s.entities + in + // entity_owner[(newid,tokenid)] = owner + let new_entityIndexToOwner : entityIndexToOwner = match Map.find_opt (newid,tokenid) s.entity_owner with + | Some addr -> (failwith("already owned") : entityIndexToOwner) + | None -> Map.add (newid,tokenid) owner s.entity_owner + in + // owner_entities[owner] = Set.add (newid,tokenid) owner_entities[owner] + let new_owner_entities : owner_entities = match Map.find_opt owner s.owner_entities with + | Some et_s -> Map.update owner (Some (Set.add (newid,tokenid) et_s)) s.owner_entities + | None -> Map.add owner (Set.add (newid,tokenid) Set.empty) s.owner_entities + in + // total_supply + 1 + let new_tokens : (token_id,token) map = match Map.find_opt tokenid s.tokens with + | Some tok -> Map.update tokenid (Some {tok with total_supply=tok.total_supply+1n}) s.tokens + | None -> (failwith("unknown token_id") : (token_id,token) map) + in + + (([] : operation list), { s with entities=new_entities; entity_owner=new_entityIndexToOwner; owner_entities=new_owner_entities; tokens=new_tokens}) + else + (failwith("only admin can do it") : return) + +let balance_of (param, s : balance_of_param_michelson * storage) : return = + let param_bo_aux : balance_of_param_aux = Layout.convert_from_right_comb(param: balance_of_param_michelson) in + let get_balance = fun ( i : balance_of_request_michelson) -> + let bor : balance_of_request = Layout.convert_from_right_comb(i) in + match Map.find_opt bor.owner s.owner_entities with + | Some et_s -> + let requested_token_id : token_id = bor.token_id in + // loop on et_s set of (entity_id * token_id) + let compute_balance = fun (acc,et : (nat * entity_key)) -> + if et.1 = requested_token_id then + acc + 1n + else + acc + in + let computed_balance : nat = Set.fold compute_balance et_s 0n in + { request = Layout.convert_to_right_comb(bor) ; balance =computed_balance } + | None -> (failwith("unknown owner") : balance_of_response_aux) + in + let balance_of_callback_param : balance_of_response_aux list = List.map get_balance param_bo_aux.requests in + let convert = fun ( r : balance_of_response_aux) -> Layout.convert_to_right_comb(r) in + let balance_of_callback_param_michelson : balance_of_response_michelson list = List.map convert balance_of_callback_param in + // sending back the processed map of balance requests/responses + let destination: (balance_of_response_michelson list) contract = param_bo_aux.callback in + let balance_of_response_operation : operation = Tezos.transaction balance_of_callback_param_michelson 0mutez destination in + ([balance_of_response_operation], s) + +let total_supply(params, s: total_supply_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith("unknown token_id") : total_supply_response) + in + let responses : total_supply_response list = List.map get_total_supply token_ids in + let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in + let ret : total_supply_response_michelson list = List.map convert responses in + let destination: (total_supply_response_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let token_metadata(params, s: token_metadata_param_michelson * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let p : token_metadata_param = Layout.convert_from_right_comb(params: token_metadata_param_michelson) in + let token_ids : token_id list = p.token_ids in + let get_metadata = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> v.metadata + | None -> (failwith("unknown token_id") : token_metadata) + in + let responses : token_metadata list = List.map get_metadata token_ids in + let convert = fun ( r : token_metadata) -> Layout.convert_to_right_comb(r) in + let ret : token_metadata_michelson list = List.map convert responses in + let destination: (token_metadata_michelson list) contract = p.callback in + let op : operation = Tezos.transaction ret 0mutez destination in + ([ op ], s) + +let transfer(params, s: transfer_michelson list * storage) : return = + if s.paused = true then + (failwith("contract in pause") : return) + else + let apply_transfer = fun (eo_oe,i : (entityIndexToOwner * owner_entities) * transfer_michelson) -> + let t_aux : transfer_aux = Layout.convert_from_right_comb(i: transfer_michelson) in + let from_ : address = t_aux.from_ in + let result_ledger : (entityIndexToOwner * owner_entities) = + if Tezos.sender = from_ or Tezos.sender = s.administrator or Set.mem {owner=from_;operator=Tezos.sender} s.operators then + let transfers : transfer_destination_michelson list = t_aux.txs in + let apply_transfer_destination = fun (acc,j : ((entityIndexToOwner * owner_entities) * transfer_destination_michelson)) -> + let transfer_destination : transfer_destination = Layout.convert_from_right_comb(j: transfer_destination_michelson) in + let to_ : address = transfer_destination.to_ in + let tokenid : token_id = transfer_destination.token_id in + + // hack + let transfered_entity : nat = transfer_destination.amount in + + let current_entity_owner : entityIndexToOwner = acc.0 in + let current_owner_entities : owner_entities = acc.1 in + + //asset(entity_owner[entity_id] = from) + let assert_owner_from : bool = match Map.find_opt (transfered_entity,tokenid) current_entity_owner with + | None -> (failwith("entity does not exist") : bool) + | Some ownr -> ownr = from_ + in + if assert_owner_from then + //entity_owner[entity_id] = to + let update_entity_owner : entityIndexToOwner = Map.update (transfered_entity,tokenid) (Some to_) current_entity_owner in + + let owner_entities_from : entity_key set = match Map.find_opt from_ current_owner_entities with + | None -> (failwith(not_owner) : entity_key set) + | Some et_s -> Set.remove (transfered_entity,tokenid) et_s + in + let updated_owner_entities : owner_entities = Map.update from_ (Some owner_entities_from) current_owner_entities in + let owner_entities_to : entity_key set = match Map.find_opt to_ updated_owner_entities with + | None -> (failwith(not_owner) : entity_key set) + | Some et_s -> Set.add (transfered_entity,tokenid) et_s + in + let updated_owner_entities2 : owner_entities = Map.update to_ (Some owner_entities_to) updated_owner_entities in + (update_entity_owner, updated_owner_entities2) + else + (failwith(not_owner) : (entityIndexToOwner * owner_entities)) + in + let result : (entityIndexToOwner * owner_entities) = List.fold apply_transfer_destination transfers eo_oe in + result + else + (failwith(not_owner ^ not_operator) : (entityIndexToOwner * owner_entities)) + in + result_ledger + in + let new_ledger : (entityIndexToOwner * owner_entities) = List.fold apply_transfer params (s.entity_owner, s.owner_entities) in + (([] : operation list), {s with entity_owner=new_ledger.0;owner_entities=new_ledger.1}) + +let update_operators (params,s : (update_operator_michelson list * storage)) : return = + if Tezos.sender <> s.administrator then + (failwith("operators can only be modified by the admin") : return) + else + let convert = fun (i : update_operator_michelson) -> (Layout.convert_from_right_comb(i) : update_operator_aux) in + let params_aux_list : update_operator_aux list = List.map convert params in + let apply_order = fun (acc,j : operator_param set * update_operator_aux) -> + match j with + | Add_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.add p acc + else + (failwith("notautorized !!!! ") : operator_param set) + | Remove_operator opm -> + let p : operator_param = Layout.convert_from_right_comb(opm) in + if (Tezos.sender = p.owner or Tezos.sender = s.administrator) then + Set.remove p acc + else + (failwith("notautorized !!!! ") : operator_param set) + in + let new_operators : operator_param set = List.fold apply_order params_aux_list s.operators in + (([] : operation list), {s with operators=new_operators}) + +let is_operator(params,s : (is_operator_param_michelson * storage)) : return = + let p : is_operator_param_aux = Layout.convert_from_right_comb(params) in + let op_param : operator_param = Layout.convert_from_right_comb(p.operator) in + let response_aux : is_operator_response_aux = {operator=p.operator;is_operator=Set.mem op_param s.operators} in + let response : is_operator_response_michelson = Layout.convert_to_right_comb(response_aux) in + let destination: (is_operator_response_michelson) contract = p.callback in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let send_permissions_descriptor(param,s : (permissions_descriptor_michelson contract * storage)) : return = + let response : permissions_descriptor_michelson = Layout.convert_to_right_comb(s.permissions_descriptor) in + let destination: permissions_descriptor_michelson contract = param in + let op : operation = Tezos.transaction response 0mutez destination in + ([ op ], s) + +let main (param,s : total_entry_points * storage) : return = + match param with + | M_left fa2_ep -> (match fa2_ep with + | Transfer l -> transfer (l, s) + | Balance_of p -> balance_of (p, s) + | Total_supply p -> total_supply (p,s) + | Token_metadata p -> token_metadata (p,s) + | Permissions_descriptor callback -> send_permissions_descriptor (callback, s) + | Update_operators l -> update_operators (l,s) + | Is_operator o -> is_operator (o,s) + ) + | M_right specific_ep -> (match specific_ep with + | Set_pause p -> set_pause (p,s) + | Set_administrator p -> set_administrator (p,s) + | Mint ent -> mint (ent,s) + ) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterLambda/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterLambda/course.md index 0c785bf..ea84aa8 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterLambda/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterLambda/course.md @@ -10,10 +10,7 @@ Tezos as a public blockchain expects that contracts should have same behaviour f We call *antipattern* when a smart contract have special role (admin) or smart contract that may be evolving (changing rules of the smart contract). The need to modify the behaviour of a smart contract emerges when for exemple the law of the country has changed and you need to apply the same changes to the rules of your smart contract. -One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by - * *Versioning by re-emission* - * *Versioning by contract communication* : - * *Versioning by lambda* : +One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by migrating sstorage information through transactions, or by forcing the new contract to request storage data from from the old contract or by customizing the contract implementation. In this chapter we will focus on the third solution. ### Versioning by re-emission @@ -29,11 +26,11 @@ Versioning can be done by writing a single smart contract that can change its pr ## Lambda -So the idea is to : -* define an anonymous function in the storage which is called in entrypoint -* write an entrypoint that allow to change implementation of this anonymous function +Changing the behavior of a smart contract can be done by customizing the implementation through lambda's function. -Let's consider the "starmap" smart contract +So the idea is to define an anonymous function in the storage which is called in entrypoint and write an entrypoint that allow to change implementation of this anonymous function. + +Let's consider the "starmap" smart contract : ``` // starmap.ligo type coordinates = { x : int; y : int; z : int } diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterMultisig/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterMultisig/course.md index aad2c68..e397046 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterMultisig/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterMultisig/course.md @@ -12,7 +12,7 @@ When invoking a smart contract, an entrypoint is called and usually an action is The purpose of a multi-signature pattern is to execute an action when all preconditions has been verified. The action that need to be executed depends on the smart contract logic. The mutli-signature implementation can be done in a single contract with the smart contract logic or in a separated contract like a proxy contract (which emits transactions to the contract containg the logic). -### rules +### Rules The multi-signature pattern can be described with this set of rules : diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterLambda/course.md b/src/frontend/src/pages/Chapters/Pascal/ChapterLambda/course.md index ab665b8..ae9e2d8 100644 --- a/src/frontend/src/pages/Chapters/Pascal/ChapterLambda/course.md +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterLambda/course.md @@ -10,10 +10,7 @@ Tezos as a public blockchain expects that contracts should have same behaviour f We call *antipattern* when a smart contract have special role (admin) or smart contract that may be evolving (changing rules of the smart contract). The need to modify the behaviour of a smart contract emerges when for exemple the law of the country has changed and you need to apply the same changes to the rules of your smart contract. -One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by - * *Versioning by re-emission* - * *Versioning by contract communication* : - * *Versioning by lambda* : +One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by migrating sstorage information through transactions, or by forcing the new contract to request storage data from from the old contract or by customizing the contract implementation. In this chapter we will focus on the third solution. ### Versioning by re-emission @@ -29,11 +26,12 @@ Versioning can be done by writing a single smart contract that can change its pr ## Lambda -So the idea is to : -* define an anonymous function in the storage which is called in entrypoint -* write an entrypoint that allow to change implementation of this anonymous function +Changing the behavior of a smart contract can be done by customizing the implementation through lambda's function. + +So the idea is to define an anonymous function in the storage which is called in entrypoint and write an entrypoint that allow to change implementation of this anonymous function. + +Let's consider the "starmap" smart contract : -Let's consider the "starmap" smart contract ``` // starmap.ligo type coordinates is record [ x : int; y : int; z : int ] diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterMultisig/course.md b/src/frontend/src/pages/Chapters/Pascal/ChapterMultisig/course.md index be1f9ca..75993b9 100644 --- a/src/frontend/src/pages/Chapters/Pascal/ChapterMultisig/course.md +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterMultisig/course.md @@ -12,7 +12,7 @@ When invoking a smart contract, an entrypoint is called and usually an action is The purpose of a multi-signature pattern is to execute an action when all preconditions has been verified. The action that need to be executed depends on the smart contract logic. The mutli-signature implementation can be done in a single contract with the smart contract logic or in a separated contract like a proxy contract (which emits transactions to the contract containg the logic). -### rules +### Rules The multi-signature pattern can be described with this set of rules : diff --git a/src/frontend/src/pages/Chapters/Reason/ChapterLambda/course.md b/src/frontend/src/pages/Chapters/Reason/ChapterLambda/course.md index 396197a..cf287ce 100644 --- a/src/frontend/src/pages/Chapters/Reason/ChapterLambda/course.md +++ b/src/frontend/src/pages/Chapters/Reason/ChapterLambda/course.md @@ -10,10 +10,7 @@ Tezos as a public blockchain expects that contracts should have same behaviour f We call *antipattern* when a smart contract have special role (admin) or smart contract that may be evolving (changing rules of the smart contract). The need to modify the behaviour of a smart contract emerges when for exemple the law of the country has changed and you need to apply the same changes to the rules of your smart contract. -One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by - * *Versioning by re-emission* - * *Versioning by contract communication* : - * *Versioning by lambda* : +One could write a new smart contract (V2) and deploy it but it would imply that all existing information stored in the storage of the old smart contract (V1) would be lost. This problem can be solved by migrating sstorage information through transactions, or by forcing the new contract to request storage data from from the old contract or by customizing the contract implementation. In this chapter we will focus on the third solution. ### Versioning by re-emission @@ -29,11 +26,12 @@ Versioning can be done by writing a single smart contract that can change its pr ## Lambda -So the idea is to : -* define an anonymous function in the storage which is called in entrypoint -* write an entrypoint that allow to change implementation of this anonymous function +Changing the behavior of a smart contract can be done by customizing the implementation through lambda's function. + +So the idea is to define an anonymous function in the storage which is called in entrypoint and write an entrypoint that allow to change implementation of this anonymous function. + +Let's consider the "starmap" smart contract : -Let's consider the "starmap" smart contract ``` // starmap.ligo type coordinates = { x : int, y : int, z : int } diff --git a/src/frontend/src/pages/Chapters/Reason/ChapterMultisig/course.md b/src/frontend/src/pages/Chapters/Reason/ChapterMultisig/course.md index 994432b..ef9d2ec 100644 --- a/src/frontend/src/pages/Chapters/Reason/ChapterMultisig/course.md +++ b/src/frontend/src/pages/Chapters/Reason/ChapterMultisig/course.md @@ -12,7 +12,7 @@ When invoking a smart contract, an entrypoint is called and usually an action is The purpose of a multi-signature pattern is to execute an action when all preconditions has been verified. The action that need to be executed depends on the smart contract logic. The mutli-signature implementation can be done in a single contract with the smart contract logic or in a separated contract like a proxy contract (which emits transactions to the contract containg the logic). -### rules +### Rules The multi-signature pattern can be described with this set of rules : From d8fe66472a6ec7bc6f11bdcb1a797df3f758df14 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Fri, 19 Jun 2020 18:25:33 +0200 Subject: [PATCH 12/16] [Cameligo] chapter FA2 (part2) correction on NFT exemple + exercice --- .../Camel/ChapterFA20Permission/course.md | 22 ++++++++++------- .../Camel/ChapterFA20Permission/exercise.cmd | 8 ++++--- .../non_fungible_token.mligo | 24 ++++++++++--------- .../Camel/ChapterFA20Permission/solution.cmd | 5 ++++ 4 files changed, 37 insertions(+), 22 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution.cmd diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md index 8c96355..74e1abc 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/course.md @@ -176,17 +176,23 @@ Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) ## Your mission We are working on a non_fungible/multi-asset token. -Our NFT "token" is almost ready but to allow a new rule. We need A to transfert a token taken from B account and send it to C account. +Our NFT "token" is almost ready but to allow a new rule. We need Bob to transfert a token taken from Vera account and send it to Alice account. 1- First we have to set the right operator policy to authorize delegation when deploying the contract. We want you to prepare the initial state of storage. Write the _ligo compile-storage_ command for the *token* contract with following recommandations : - * operator transfer is authorized, - * A account has a balance set to 100 - * B account has a balance set to 100 - * C account has a balance set to 100 - * D is the administrator of the contract + + * Jay 's account address is "tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" + * Alice's account address is "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" + * Bob's account address is "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU" + * Vera's account address is "tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv" + * operator transfer is authorized, + * Bob account has no token + * Vera account is owner of the token 1 + * Alice account has no token + * Jay is the administrator of the contract + * the token type transfered is 0 (token_id) -2- Write the _ligo dry-run_ command for authorizing A to transfer token taken from B account, transaction emitted by D. (reuse the storage you made on step 1) +2- Write the _ligo dry-run_ command for authorizing Bob to transfer token taken from Vera account, transaction emitted by Jay. (reuse the storage you made on step 1) -3- Write the _ligo dry-run_ command for simulating the transfer of 1 mutez from B'account to c's account, transaction emitted by A. You will have to modify the storage to take step 2 into account. +3- Write the _ligo dry-run_ command for simulating the transfer of 1 mutez from Vera'account to Alice's account, transaction emitted by A. You will have to modify the storage to take step 2 into account. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd index bb78a16..3d8e72b 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/exercise.cmd @@ -1,3 +1,5 @@ -ligo compile-storage non_fungible_token.mligo main '' -ligo dry-run shiptoken.ligo 'Update_operators([{owner=TZ1;operator=TZ1}])' '' -ligo dry-run --sender=A shiptoken.ligo 'Transfer(B,C,token_id)' \ No newline at end of file +ligo compile-storage shiptoken.mligo main '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=(Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' + +ligo dry-run --sender=tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J shiptoken.mligo main 'Fa2 (Update_operators([ (Layout.convert_to_right_comb( Add_operator((Layout.convert_to_right_comb( ({owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address);operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)}: operator_param) ) : operator_param_michelson)) ) : update_operator_michelson) ]))' '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=(Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU shiptoken.mligo main 'Fa2 (Transfer( [ (Layout.convert_to_right_comb( ({from_=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); txs=[ (Layout.convert_to_right_comb( ({to_=("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address); token_id=0n; amount=1n }: transfer_destination)) : transfer_destination_michelson) ]} : transfer_aux)): transfer_michelson) ]))' '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=Set.add ({owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address);operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)}: operator_param) (Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo index e845d6a..1d18001 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/non_fungible_token.mligo @@ -1,5 +1,5 @@ -#include "tzip/proposals/tzip-12/fa2_interface.mligo" -#include "tzip/proposals/tzip-12/fa2_errors.mligo" +#include "../tzip/proposals/tzip-12/fa2_interface.mligo" +#include "../tzip/proposals/tzip-12/fa2_errors.mligo" type entity = { code : string; @@ -10,7 +10,6 @@ type entity_key = (entity_id * token_id) type entities = (entity_key, entity) map type entityIndexToOwner = (entity_key, address) map type owner_entities = (address, entity_key set) map -//type owner_entities_count = (address, nat) map type token = { total_supply : nat; @@ -35,7 +34,9 @@ type entry_points = | Set_administrator of address | Mint of (entity * address * token_id) -type total_entry_points = (fa2_entry_points, "fa2_ep", entry_points, "specific_ep") michelson_or +type nft_entry_points = + | Fa2 of fa2_entry_points + | Nft of entry_points let set_pause(param,s : bool * storage): return = if Tezos.sender = s.administrator then @@ -51,9 +52,10 @@ let set_administrator(param,s : address * storage): return = let mint (param,s : (entity * address * token_id) * storage) : return = if Tezos.sender = s.administrator then - let new_ent : entity = param.0 in - let owner : address = param.1 in - let tokenid : token_id = param.2 in + // let new_ent : entity = param.0 in + // let owner : address = param.1 in + // let tokenid : token_id = param.2 in + let new_ent, owner, tokenid = param in // NEVER burn an entity or entity_id will be provided an existing id (mint burn mint) let newid : entity_id = match Map.find_opt tokenid s.tokens with | Some tok -> tok.total_supply + 1n @@ -237,9 +239,9 @@ let send_permissions_descriptor(param,s : (permissions_descriptor_michelson cont let op : operation = Tezos.transaction response 0mutez destination in ([ op ], s) -let main (param,s : total_entry_points * storage) : return = +let main (param,s : nft_entry_points * storage) : return = match param with - | M_left fa2_ep -> (match fa2_ep with + | Fa2 fa2_ep -> (match fa2_ep with | Transfer l -> transfer (l, s) | Balance_of p -> balance_of (p, s) | Total_supply p -> total_supply (p,s) @@ -248,8 +250,8 @@ let main (param,s : total_entry_points * storage) : return = | Update_operators l -> update_operators (l,s) | Is_operator o -> is_operator (o,s) ) - | M_right specific_ep -> (match specific_ep with + | Nft specific_ep -> (match specific_ep with | Set_pause p -> set_pause (p,s) | Set_administrator p -> set_administrator (p,s) | Mint ent -> mint (ent,s) - ) + ) \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution.cmd new file mode 100644 index 0000000..3d8e72b --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution.cmd @@ -0,0 +1,5 @@ +ligo compile-storage shiptoken.mligo main '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=(Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' + +ligo dry-run --sender=tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J shiptoken.mligo main 'Fa2 (Update_operators([ (Layout.convert_to_right_comb( Add_operator((Layout.convert_to_right_comb( ({owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address);operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)}: operator_param) ) : operator_param_michelson)) ) : update_operator_michelson) ]))' '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=(Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU shiptoken.mligo main 'Fa2 (Transfer( [ (Layout.convert_to_right_comb( ({from_=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); txs=[ (Layout.convert_to_right_comb( ({to_=("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address); token_id=0n; amount=1n }: transfer_destination)) : transfer_destination_michelson) ]} : transfer_aux)): transfer_michelson) ]))' '{paused=false; entities=Map.literal [((1n,0n),{name="first";code="040233"})]; entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; owner_entities=Map.literal [(("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set))]; tokens=Map.literal [(0n,{total_supply=0n;metadata={token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}})]; operators=Set.add ({owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address);operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)}: operator_param) (Set.empty : operator_param set); administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); permissions_descriptor={operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); receiver=Layout.convert_to_right_comb(Owner_no_hook); sender=Layout.convert_to_right_comb(Owner_no_hook); custom=(None : custom_permission_policy_michelson option) } }' From 8009df5e5316e9786c48bd1c4ad5b63f2d47a657 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Tue, 23 Jun 2020 16:45:46 +0200 Subject: [PATCH 13/16] [Cameligo] chapter FA2 Hook --- .../Chapters/Camel/ChapterFA20Hook/course.md | 170 +++++++++------- .../Camel/ChapterFA20Hook/exercise.mligo | 75 +++++++ .../Camel/ChapterFA20Hook/fa2_core.mligo | 161 +++++++++++++++ .../Chapters/Camel/ChapterFA20Hook/index.ts | 30 +++ .../Camel/ChapterFA20Hook/solution.mligo | 84 ++++++++ .../ChapterFA20Hook/tzip-12/fa2_errors.mligo | 49 +++++ .../ChapterFA20Hook/tzip-12/fa2_hook.mligo | 32 +++ .../tzip-12/fa2_interface.mligo | 183 ++++++++++++++++++ .../tzip-12/lib/fa2_convertors.mligo | 154 +++++++++++++++ .../tzip-12/lib/fa2_operator_lib.mligo | 88 +++++++++ .../tzip-12/lib/fa2_owner_hooks_lib.mligo | 130 +++++++++++++ .../tzip-12/lib/fa2_transfer_hook_lib.mligo | 49 +++++ 12 files changed, 1131 insertions(+), 74 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/exercise.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/fa2_core.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/index.ts create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/solution.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_errors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_hook.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_interface.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_convertors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_owner_hooks_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.mligo diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md index 98df4ee..98b2ae5 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/course.md @@ -55,9 +55,11 @@ In addition to the hook standard, the FA2 standard provides helper functions to #### FA2 standard hook library +##### Register FA2 core with Hook permission contract + Some helpers functions has been gatthered in a hook library which help defining hooks when implementing a FA2 contract. This library contains following functions and type alias : -The type _fa2_registry_ is a set of address +The type *fa2_registry* is a set of address. the function *get_hook_entrypoint* retrieves the contract interface of entrypoint "%tokens_transferred_hook" for a given contract address @@ -71,6 +73,40 @@ the function *create_register_hook_op* sends a transaction to a FA2 contract (ha the function *validate_hook_call* ensures an address in registered in the registry (set of address). + +##### Transfer Hooks + +The function *owners_transfer_hook* defined in the library generates a list of Tezos operations invoking sender and receiver hooks according to +the policies defined by the permissions descriptor. + + +The hook pattern depends on the permission policy. A transfer hook may be unwanted, optional or required. +If the policy requires a owner hook then the token owner contract MUST implement an entry point "tokens_received". Otherwise transfer is not allowed. +If the policy optionnaly accepts a owner hook then the token owner contract MAY implement an entry point "tokens_received". Otherwise transfer is allowed. + +It is the same for permission policies including senders, the entry point *tokens_sent* may need to be implemented. + +In case of a Transfer, if permission policies expect a hook, then the token owners MUST implement *fa2_token_receiver*, and *fa2_token_sender* interfaces. This implies that token'owner contract must have entry points *tokens_received* and *token_sent*. If these entry points fail the transfer is rejected. + + +##### Transfer Hooks entry points + +The library defines some helper functions + +The function *to_receiver_hook* retrieves the entry point *"%tokens_received"* for a given _address_. It enables to check if the *fa2_token_receiver* interface is implemented. + +The function *to_sender_hook* retrieves the entry point *"%tokens_sent"* for a given _address_. It enables to check if the *fa2_token_sender* interface is implemented. + +These two functions return a variant *hook_result* type. If variant value is *Hook_contract* then the entrypoint exists an is provided. If variant value is Hook_undefined then the entry point is not implemented and a message error is provided. + +``` +type hook_result = + | Hook_contract of transfer_descriptor_param_michelson contract + | Hook_undefined of string +``` + + + #### Hook Rules FA2 implementation with the transfer hook pattern recquires following rules: @@ -102,109 +138,95 @@ invoked hook fails, the whole transfer transaction MUST fail. FA2 token contract implements mint and burn operations, these operations MUST invoke a transfer hook as well. -#### Implementation of a custom hook +#### Implementation of a hook permission contract -Let's see an example of FA2 implementation. The following smart contract implements a token where transfer receiver must be in a whitelist. This whitelisting is done via a tranfer hook. -It uses a combination of a receiver white list and *fa2_token_receiver* interface. -Transfer is permitted if a receiver address is in the receiver white list OR implements *fa2_token_receiver* interface. +Let's see an example of FA2 Hook pattern implementation. The following smart contract implements a hook permission contract + +Owners transfer hooks are triggered by the *owners_transfer_hook* function. If a receiver address implements *fa2_token_receiver* interface, its *tokens_received* entry point must be called. +If a sender address implements *fa2_token_sender* interface, its *tokens_sent* entry point must be called. + ``` -#include "../lib/fa2_hook_lib.mligo" -#include "../lib/fa2_behaviors.mligo" +(** +Implementation of a generic permission transfer hook that supports sender/receiver +hooks. Contract behavior is driven by the permissions descriptor value in the +contract storage and its particular settings for `sender` and `receiver` policies. +*) +#include "../lib/fa2_transfer_hook_lib.mligo" +#include "../lib/fa2_owner_hooks_lib.mligo" type storage = { fa2_registry : fa2_registry; - receiver_whitelist : address set; -} - -let custom_validate_receivers (p, wl : transfer_descriptor_param * (address set)) - : operation list = - let get_receiver : get_owners = fun (tx : transfer_descriptor) -> - List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in - let receivers = get_owners_from_batch (p.batch, get_receiver) in - - Set.fold - (fun (ops, r : (operation list) * address) -> - let hook, err = to_sender_hook r in - match hook with - | Some h -> - let pm = transfer_descriptor_param_to_michelson p in - let op = Operation.transaction pm 0mutez h in - op :: ops - | None -> - if Set.mem r wl - then ops - else (failwith err : operation list) - ) - receivers ([] : operation list) - -let custom_transfer_hook (p, s : transfer_descriptor_param * storage) : operation list = - custom_validate_receivers (p, s.receiver_whitelist) - - -let get_policy_descriptor (u : unit) : permissions_descriptor = - { - operator = Owner_or_operator_transfer; - sender = Owner_no_hook; - receiver = Owner_no_hook ; (* overridden by the custom policy *) - custom = Some { - tag = "receiver_hook_and_whitelist"; - config_api = (Some Current.self_address); - }; - } - -type config_whitelist = - | Add_receiver_to_whitelist of address set - | Remove_receiver_from_whitelist of address set - -let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) - : address set = - match cfg with - | Add_receiver_to_whitelist rs -> - Set.fold - (fun (l, a : (address set) * address) -> Set.add a l) - rs wl - | Remove_receiver_from_whitelist rs -> - Set.fold - (fun (l, a : (address set) * address) -> Set.remove a l) - rs wl + descriptor : permissions_descriptor; +} type entry_points = - | Tokens_transferred_hook of transfer_descriptor_param + | Tokens_transferred_hook of transfer_descriptor_param_michelson | Register_with_fa2 of fa2_with_hook_entry_points contract - | Config_receiver_whitelist of config_whitelist let main (param, s : entry_points * storage) : (operation list) * storage = match param with - | Tokens_transferred_hook p -> - // verify s.fa2_registry contains p.fa2 address otherwise throw exception - let u = validate_hook_call (p.fa2, s.fa2_registry) in - let ops = custom_transfer_hook (p, s) in + | Tokens_transferred_hook pm -> + let p = transfer_descriptor_param_from_michelson pm in + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let ops = owners_transfer_hook + ({ligo_param = p; michelson_param = pm}, s.descriptor) in ops, s | Register_with_fa2 fa2 -> - let descriptor = get_policy_descriptor unit in - let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let op , new_registry = register_with_fa2 (fa2, s.descriptor, s.fa2_registry) in let new_s = { s with fa2_registry = new_registry; } in [op], new_s - | Config_receiver_whitelist cfg -> - let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in - let new_s = { s with receiver_whitelist = new_wl; } in - ([] : operation list), new_s + +(** example policies *) + +(* the policy which allows only token owners to transfer their own tokens. *) +let own_policy : permissions_descriptor = { + operator = Owner_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook; + custom = (None : custom_permission_policy option); +} ``` +Notice this Hook Permission contract contains an entry point *Register_with_fa2* to register with the FA2 core contract. + +Notice this Hook Permission contract contains an entry point *Tokens_transferred_hook* triggered when FA2 core contract receive a transfer request. This entry point triggers the owner hook transfer (sending hooks to sender and receiver and waiting for their approval or rejection). ## Your mission +We are working on a Fungible token which can handle multiple assets. We decided to implement a Hook pattern. A FA2 core contract handle all fa2 entry points (BalanceOf, Transfer, ...) and a hook permission contract which implements the validation of a transfer with some custom rules. + +1 - we want to accept a transfer if transfer receiver is registered in a whitelist. This whitelisting is done via a tranfer hook. + +2 - we want to accept a transfer if transfer receiver implements *fa2_token_receiver* interface. + +If a receiver address implements *fa2_token_receiver* interface, its *tokens_received* entry point must be called. + + +Complete the hook permission smart contract by implementing our custom rules on receivers. Transfer is permitted if receiver address implements *fa2_token_receiver* interface OR a receiver address is in the receiver white list. + +1- Find receiver hook - Check if a receiver _r_ implements *fa2_token_receiver* interface, using *to_receiver_hook* function and a _match_ operator. + +2- Retrieve hook - if the receiver _r_ implements *fa2_token_receiver* interface, introduce variable _h_ as hook entry point. + +3- Prepare parameter - cast parameter _p_ into type *transfer_descriptor_param_to_michelson* and store the result in a new variable _pm_ + +4- Call the entry point - create a variable _op_ of type *operation* which is a transaction sending variable _pm_ and no mutez to the retrieved entry point _h_ + +5- Return transactions - add this newly created operation _op_ in the returned list of operation _ops_ (and return _ops_) +6- if the receiver _r_ does not implement *fa2_token_receiver* interface, response of *to_receiver_hook* provided an error message with variable _err_. -1- We want you to simulate the transfer of 2 TAT (Tezos Academy Token) to *alice*. Write a ligo command line for preparing a simulated storage where you (tz1SdT62G8tQp9fdHh4f2m4VtL8aGG6NUcmJ) possess 1000000 of token and no allowances. +7- Check if receiver _r_ is registered in the whitelist _wl_. +8- If it is the case , everything is fine, just return the returned list of operation _ops_. +9- Otherwise throw an exception with _err_ message. Don't forget to cast the exception. diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/exercise.mligo new file mode 100644 index 0000000..581fffa --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/exercise.mligo @@ -0,0 +1,75 @@ +#include "tzip-12/lib/fa2_transfer_hook_lib.mligo" +#include "tzip-12/lib/fa2_owner_hooks_lib.mligo" + + +type storage = { + fa2_registry : fa2_registry; + receiver_whitelist : address set; +} + +let custom_validate_receivers (p, wl : transfer_descriptor_param * (address set)) + : operation list = + let get_receiver : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in + let receivers = get_owners_from_batch (p.batch, get_receiver) in + + Set.fold + (fun (ops, r : (operation list) * address) -> + // Type your solution below + ) + receivers ([] : operation list) + +let custom_transfer_hook (p, s : transfer_descriptor_param * storage) : operation list = + custom_validate_receivers (p, s.receiver_whitelist) + + +let get_policy_descriptor (u : unit) : permissions_descriptor = + { + operator = Owner_or_operator_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook ; (* overridden by the custom policy *) + custom = Some { + tag = "receiver_hook_and_whitelist"; + config_api = (Some Current.self_address); + }; + } + +type config_whitelist = + | Add_receiver_to_whitelist of address set + | Remove_receiver_from_whitelist of address set + +let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) + : address set = + match cfg with + | Add_receiver_to_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.add a l) + rs wl + | Remove_receiver_from_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.remove a l) + rs wl + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_receiver_whitelist of config_whitelist + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook p -> + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let ops = custom_transfer_hook (p, s) in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = get_policy_descriptor unit in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_receiver_whitelist cfg -> + let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in + let new_s = { s with receiver_whitelist = new_wl; } in + ([] : operation list), new_s diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/fa2_core.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/fa2_core.mligo new file mode 100644 index 0000000..a06e8ce --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/fa2_core.mligo @@ -0,0 +1,161 @@ +#if !FA2_MAC_TOKEN +#define FA2_MAC_TOKEN + +#include "tzip-12/fa2_interface.mligo" +#include "tzip-12/fa2_errors.mligo" +#include "tzip-12/lib/fa2_operator_lib.mligo" +#include "tzip-12/lib/fa2_owner_hooks_lib.mligo" + +(* (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 + +(* token_id -> token_metadata *) +type token_metadata_storage = (token_id, token_metadata_michelson) big_map + +type multi_token_storage = { + ledger : ledger; + operators : operator_storage; + token_total_supply : token_total_supply; + token_metadata : token_metadata_storage; + permissions_descriptor : permissions_descriptor; +} + +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 + 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 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 owner_validator function that validates of the tokens from the particular owner can be transferred. + *) +let transfer (txs, owner_validator, storage + : (transfer list) * ((address * operator_storage) -> unit) * multi_token_storage) + : ledger = + let make_transfer = fun (l, tx : ledger * transfer) -> + let u = owner_validator (tx.from_, storage.operators) in + 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 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 get_balance (p, ledger, tokens + : balance_of_param * ledger * token_total_supply) : 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_michelson) + else + let key = r.owner, r.token_id in + let bal = get_balance_amt (key, ledger) in + let response = { request = r; balance = bal; } in + balance_of_response_to_michelson response + in + let responses = List.map to_balance p.requests in + Operation.transaction responses 0mutez p.callback + +let get_owner_hook_ops_for_descriptor (tx_descriptor, pd + : transfer_descriptor_param * permissions_descriptor) : operation list = + let hook_calls = owners_transfer_hook (tx_descriptor, pd) in + match hook_calls with + | [] -> ([] : operation list) + | h :: t -> + let tx_descriptor_michelson = transfer_descriptor_param_to_michelson tx_descriptor in + List.map (fun(call: hook_entry_point) -> + Operation.transaction tx_descriptor_michelson 0mutez call) + hook_calls + +let get_owner_hook_ops (txs, pd : (transfer list) * permissions_descriptor) : operation list = + let tx_descriptors = transfers_to_descriptors txs in + let tx_descriptor : transfer_descriptor_param = { + operator = Tezos.sender; + batch = tx_descriptors; + } in + get_owner_hook_ops_for_descriptor (tx_descriptor, pd) + +let fa2_main (param, storage : fa2_entry_points * multi_token_storage) + : (operation list) * multi_token_storage = + match param with + | Transfer txs_michelson -> + (* convert transfer batch into `transfer_descriptor` batch *) + let txs = transfers_from_michelson txs_michelson in + (* + will validate that a sender is either `from_` parameter of each transfer + or a permitted operator for the owner `from_` address. + *) + let validator = make_default_operator_validator Tezos.sender in + let new_ledger = transfer (txs, validator, storage) in + let new_storage = { storage with ledger = new_ledger; } in + + let hook_ops = get_owner_hook_ops (txs, storage.permissions_descriptor) in + + (hook_ops), new_storage + + | Balance_of pm -> + let p = balance_of_param_from_michelson pm in + let op = get_balance (p, storage.ledger, storage.token_total_supply) in + [op], storage + + | Update_operators updates_michelson -> + let updates = operator_updates_from_michelson updates_michelson in + let updater = Tezos.sender in + let process_update = (fun (ops, update : operator_storage * update_operator) -> + let u = validate_update_operators_by_owner (update, updater) in + update_operators (update, ops) + ) in + let new_ops = + List.fold process_update updates storage.operators in + let new_storage = { storage with operators = new_ops; } in + ([] : operation list), new_storage + + | Token_metadata_registry callback -> + (* the contract maintains token metadata in its storage - `token_metadata` big_map *) + let callback_op = Operation.transaction Tezos.self_address 0mutez callback in + [callback_op], storage + +type fa2_multi_token_entry_points = + | FA2 of fa2_entry_points + | Permissions_descriptor of permissions_descriptor_michelson contract + +let get_permissions_descriptor (callback, storage + : permissions_descriptor_michelson contract * multi_token_storage) + : (operation list) * multi_token_storage = + let pdm = permissions_descriptor_to_michelson storage.permissions_descriptor in + let callback_op = Operation.transaction pdm 0mutez callback in + [callback_op], storage + +let fa2_multi_token_main (param, storage : fa2_multi_token_entry_points * multi_token_storage) + : (operation list) * multi_token_storage = + match param with + | FA2 fa2_param -> fa2_main (fa2_param, storage) + | Permissions_descriptor callback -> get_permissions_descriptor(callback, storage) +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/index.ts b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/index.ts new file mode 100644 index 0000000..3b1880a --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/index.ts @@ -0,0 +1,30 @@ +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import course from "!raw-loader!./course.md"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import exercise from "!raw-loader!./exercise.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import solution from "!raw-loader!./solution.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support1 from "!raw-loader!./tzip-12/lib/fa2_convertors.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support2 from "!raw-loader!./tzip-12/lib/fa2_operator_lib.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support3 from "!raw-loader!./tzip-12/lib/fa2_owner_hooks_lib.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support4 from "!raw-loader!./tzip-12/fa2_interface.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support5 from "!raw-loader!./tzip-12/fa2_errors.mligo"; + +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support6 from "!raw-loader!./fa2_core.mligo"; + +export const data = { course, exercise, solution, support1, support2, support3, support4, support5, support6 }; diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/solution.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/solution.mligo new file mode 100644 index 0000000..59aa417 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/solution.mligo @@ -0,0 +1,84 @@ +#include "tzip-12/lib/fa2_transfer_hook_lib.mligo" +#include "tzip-12/lib/fa2_owner_hooks_lib.mligo" + + +type storage = { + fa2_registry : fa2_registry; + receiver_whitelist : address set; +} + +let custom_validate_receivers (p, wl : transfer_descriptor_param * (address set)) + : operation list = + let get_receiver : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in + let receivers = get_owners_from_batch (p.batch, get_receiver) in + + Set.fold + (fun (ops, r : (operation list) * address) -> + // Type your solution below + match to_receiver_hook r with + | Hook_contract h -> + let pm = transfer_descriptor_param_to_michelson p in + let op = Operation.transaction pm 0mutez h in + op :: ops + | Hook_undefined err -> + if Set.mem r wl + then ops + else (failwith err : operation list) + ) + receivers ([] : operation list) + +let custom_transfer_hook (p, s : transfer_descriptor_param * storage) : operation list = + custom_validate_receivers (p, s.receiver_whitelist) + + +let get_policy_descriptor (u : unit) : permissions_descriptor = + { + operator = Owner_or_operator_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook ; (* overridden by the custom policy *) + custom = Some { + tag = "receiver_hook_and_whitelist"; + config_api = (Some Current.self_address); + }; + } + +type config_whitelist = + | Add_receiver_to_whitelist of address set + | Remove_receiver_from_whitelist of address set + +let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) + : address set = + match cfg with + | Add_receiver_to_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.add a l) + rs wl + | Remove_receiver_from_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.remove a l) + rs wl + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_receiver_whitelist of config_whitelist + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook p -> + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let ops = custom_transfer_hook (p, s) in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = get_policy_descriptor unit in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_receiver_whitelist cfg -> + let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in + let new_s = { s with receiver_whitelist = new_wl; } in + ([] : operation list), new_s diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_errors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_errors.mligo new file mode 100644 index 0000000..35a8ab8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_errors.mligo @@ -0,0 +1,49 @@ +#if !FA2_ERRORS +#define FA2_ERRORS + +(** One of the specified `token_id`s is not defined within the FA2 contract *) +let fa2_token_undefined = "FA2_TOKEN_UNDEFINED" +(** +A token owner does not have sufficient balance to transfer tokens from +owner's account +*) +let fa2_insufficient_balance = "FA2_INSUFFICIENT_BALANCE" +(** A transfer failed because of `operator_transfer_policy == No_transfer` *) +let fa2_tx_denied = "FA2_TX_DENIED" +(** +A transfer failed because `operator_transfer_policy == Owner_transfer` and it is +initiated not by the token owner +*) +let fa2_not_owner = "FA2_NOT_OWNER" +(** +A transfer failed because `operator_transfer_policy == Owner_or_operator_transfer` +and it is initiated neither by the token owner nor a permitted operator + *) +let fa2_not_operator = "FA2_NOT_OPERATOR" +(** +`update_operators` entry point is invoked and `operator_transfer_policy` is +`No_transfer` or `Owner_transfer` +*) +let fa2_operators_not_supported = "FA2_OPERATORS_UNSUPPORTED" +(** +Receiver hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let fa2_receiver_hook_failed = "FA2_RECEIVER_HOOK_FAILED" +(** +Sender hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let fa2_sender_hook_failed = "FA2_SENDER_HOOK_FAILED" +(** +Receiver hook is required by the permission behavior, but is not implemented by +a receiver contract + *) +let fa2_receiver_hook_undefined = "FA2_RECEIVER_HOOK_UNDEFINED" +(** +Sender hook is required by the permission behavior, but is not implemented by +a sender contract + *) +let fa2_sender_hook_undefined = "FA2_SENDER_HOOK_UNDEFINED" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_hook.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_hook.mligo new file mode 100644 index 0000000..f8dd01f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_hook.mligo @@ -0,0 +1,32 @@ +(** +Optional FA2 contract entry point to setup a transfer hook contract. +Transfer hook is one recommended design pattern to implement FA2 that enables +separation of the core token transfer logic and a permission policy. Instead of +implementing FA2 as a monolithic contract, a permission policy can be implemented +as a separate contract. Permission policy contract provides an entry point invoked +by the core FA2 contract to accept or reject a particular transfer operation (such +an entry point is called transfer hook) + *) + +#if !FA2_HOOK +#define FA2_HOOK + +#include "fa2_interface.mligo" + + +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb + +type fa2_with_hook_entry_points = + | Set_transfer_hook of set_hook_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_interface.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_interface.mligo new file mode 100644 index 0000000..5c9231a --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/fa2_interface.mligo @@ -0,0 +1,183 @@ +#if ! FA2_INTERFACE +#define FA2_INTERFACE + +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + +type transfer_michelson = transfer_aux michelson_pair_right_comb + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_request_michelson = balance_of_request michelson_pair_right_comb + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_response_aux = { + request : balance_of_request_michelson; + balance : nat; +} + +type balance_of_response_michelson = balance_of_response_aux michelson_pair_right_comb + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_aux = { + requests : balance_of_request_michelson list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_michelson = balance_of_param_aux michelson_pair_right_comb + +type operator_param = { + owner : address; + operator : address; +} + +type operator_param_michelson = operator_param michelson_pair_right_comb + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type update_operator_aux = + | Add_operator of operator_param_michelson + | Remove_operator of operator_param_michelson + +type update_operator_michelson = update_operator_aux michelson_or_right_comb + +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} + +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param = { + token_ids : token_id list; + handler : (token_metadata_michelson list) -> unit; +} + +type token_metadata_param_michelson = token_metadata_param michelson_pair_right_comb + +type fa2_entry_points = + | Transfer of transfer_michelson list + | Balance_of of balance_of_param_michelson + | Update_operators of update_operator_michelson list + | Token_metadata_registry of address contract + + +type fa2_token_metadata = + | Token_metadata of token_metadata_param_michelson + +(* permission policy definition *) + +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer + +type operator_transfer_policy_michelson = operator_transfer_policy michelson_or_right_comb + +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook + +type owner_hook_policy_michelson = owner_hook_policy michelson_or_right_comb + +type custom_permission_policy = { + tag : string; + config_api: address option; +} + +type custom_permission_policy_michelson = custom_permission_policy michelson_pair_right_comb + +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} + +type permissions_descriptor_aux = { + operator : operator_transfer_policy_michelson; + receiver : owner_hook_policy_michelson; + sender : owner_hook_policy_michelson; + custom : custom_permission_policy_michelson option; +} + +type permissions_descriptor_michelson = permissions_descriptor_aux michelson_pair_right_comb + +type fa2_entry_points_custom = + | Permissions_descriptor of permissions_descriptor_michelson contract + + +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param = { + batch : transfer_descriptor list; + operator : address; +} + +type transfer_descriptor_param_aux = { + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb + +type fa2_token_receiver = + | Tokens_received of transfer_descriptor_param_michelson + +type fa2_token_sender = + | Tokens_sent of transfer_descriptor_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_convertors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_convertors.mligo new file mode 100644 index 0000000..27b164b --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_convertors.mligo @@ -0,0 +1,154 @@ +(** +Helper function to convert FA2 entry points input parameters between their +Michelson and internal LIGO representation. + +FA2 contract implementation must conform to the Michelson entry points interface +outlined in the FA2 standard for interoperability with other contracts and off-chain +tools. + *) + +#if !FA2_CONVERTORS +#define FA2_CONVERTORS + +#include "../fa2_interface.mligo" + +let permissions_descriptor_to_michelson (d : permissions_descriptor) + : permissions_descriptor_michelson = + let aux : permissions_descriptor_aux = { + operator = Layout.convert_to_right_comb d.operator; + receiver = Layout.convert_to_right_comb d.receiver; + sender = Layout.convert_to_right_comb d.sender; + custom = match d.custom with + | None -> (None : custom_permission_policy_michelson option) + | Some c -> Some (Layout.convert_to_right_comb c) + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_to_michelson (p : transfer_descriptor) : transfer_descriptor_michelson = + let aux : transfer_descriptor_aux = { + from_ = p.from_; + txs = List.map + (fun (tx : transfer_destination_descriptor) -> + Layout.convert_to_right_comb tx + ) + p.txs; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_param_to_michelson (p : transfer_descriptor_param) + : transfer_descriptor_param_michelson = + let aux : transfer_descriptor_param_aux = { + operator = p.operator; + batch = List.map transfer_descriptor_to_michelson p.batch; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_from_michelson (p : transfer_descriptor_michelson) : transfer_descriptor = + let aux : transfer_descriptor_aux = Layout.convert_from_right_comb p in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_descriptor_michelson) -> + let tx : transfer_destination_descriptor = + Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfer_descriptor_param_from_michelson (p : transfer_descriptor_param_michelson) + : transfer_descriptor_param = + let aux : transfer_descriptor_param_aux = Layout.convert_from_right_comb p in + let b : transfer_descriptor list = + List.map transfer_descriptor_from_michelson aux.batch + in + { + operator = aux.operator; + batch = b; + } + +let transfer_from_michelson (txm : transfer_michelson) : transfer = + let aux : transfer_aux = Layout.convert_from_right_comb txm in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_michelson) -> + let tx : transfer_destination = Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfers_from_michelson (txsm : transfer_michelson list) : transfer list = + List.map transfer_from_michelson txsm + +let operator_param_from_michelson (p : operator_param_michelson) : operator_param = + let op : operator_param = Layout.convert_from_right_comb p in + op + +let operator_param_to_michelson (p : operator_param) : operator_param_michelson = + Layout.convert_to_right_comb p + +let operator_update_from_michelson (uom : update_operator_michelson) : update_operator = + let aux : update_operator_aux = Layout.convert_from_right_comb uom in + match aux with + | Add_operator opm -> Add_operator_p (operator_param_from_michelson opm) + | Remove_operator opm -> Remove_operator_p (operator_param_from_michelson opm) + +let operator_update_to_michelson (uo : update_operator) : update_operator_michelson = + let aux = match uo with + | Add_operator_p op -> Add_operator (operator_param_to_michelson op) + | Remove_operator_p op -> Remove_operator (operator_param_to_michelson op) + in + Layout.convert_to_right_comb aux + +let operator_updates_from_michelson (updates_michelson : update_operator_michelson list) + : update_operator list = + List.map operator_update_from_michelson updates_michelson + +let balance_of_param_from_michelson (p : balance_of_param_michelson) : balance_of_param = + let aux : balance_of_param_aux = Layout.convert_from_right_comb p in + let requests = List.map + (fun (rm : balance_of_request_michelson) -> + let r : balance_of_request = Layout.convert_from_right_comb rm in + r + ) + aux.requests + in + { + requests = requests; + callback = aux.callback; + } + +let balance_of_param_to_michelson (p : balance_of_param) : balance_of_param_michelson = + let aux : balance_of_param_aux = { + requests = List.map + (fun (r : balance_of_request) -> Layout.convert_to_right_comb r) + p.requests; + callback = p.callback; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_to_michelson (r : balance_of_response) : balance_of_response_michelson = + let aux : balance_of_response_aux = { + request = Layout.convert_to_right_comb r.request; + balance = r.balance; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_from_michelson (rm : balance_of_response_michelson) : balance_of_response = + let aux : balance_of_response_aux = Layout.convert_from_right_comb rm in + let request : balance_of_request = Layout.convert_from_right_comb aux.request in + { + request = request; + balance = aux.balance; + } + +let token_metas_to_michelson (ms : token_metadata list) : token_metadata_michelson list = + List.map + ( fun (m : token_metadata) -> + let mm : token_metadata_michelson = Layout.convert_to_right_comb m in + mm + ) ms + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.mligo new file mode 100644 index 0000000..eb15ab0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.mligo @@ -0,0 +1,88 @@ +(** +Reference implementation of the FA2 operator storage, config API and +helper functions +*) + +#if !FA2_OPERATOR_LIB +#define FA2_OPERATOR_LIB + +#include "fa2_convertors.mligo" +#include "../fa2_errors.mligo" + +(** +(owner, operator) -> unit +To be part of FA2 storage to manage permitted operators +*) +type operator_storage = ((address * address), unit) big_map + +(** + Updates operator storage using an `update_operator` command. + Helper function to implement `Update_operators` FA2 entry point +*) +let update_operators (update, storage : update_operator * operator_storage) + : operator_storage = + match update with + | Add_operator_p op -> + Big_map.update (op.owner, op.operator) (Some unit) storage + | Remove_operator_p op -> + Big_map.remove (op.owner, op.operator) storage + +(** +Validate if operator update is performed by the token owner. +@param updater an address that initiated the operation; usually `Tezos.sender`. +*) +let validate_update_operators_by_owner (update, updater : update_operator * address) + : unit = + let op = match update with + | Add_operator_p op -> op + | Remove_operator_p op -> op + in + if op.owner = updater then unit else failwith fa2_not_owner + +(** +Create an operator validator function based on provided operator policy. +@param tx_policy operator_transfer_policy defining the constrains on who can transfer. + *) +let make_operator_validator (tx_policy : operator_transfer_policy) + : (address * operator_storage)-> unit = + let can_owner_tx, can_operator_tx = match tx_policy with + | No_transfer -> (failwith fa2_tx_denied : bool * bool) + | Owner_transfer -> true, false + | Owner_or_operator_transfer -> true, true + in + let operator : address = Tezos.sender in + (fun (owner, ops_storage : address * operator_storage) -> + if can_owner_tx && owner = operator + then unit + else + if not can_operator_tx + then failwith fa2_not_owner + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith fa2_not_operator + ) + +(** +Default implementation of the operator validation function. +The default implicit `operator_transfer_policy` value is `Owner_or_operator_transfer` + *) +let make_default_operator_validator (operator : address) + : (address * operator_storage)-> unit = + (fun (owner, ops_storage : address * operator_storage) -> + if owner = operator + then unit + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith fa2_not_operator + ) + +(** +Validate operators for all transfers in the batch at once +@param tx_policy operator_transfer_policy defining the constrains on who can transfer. +*) +let validate_operator (tx_policy, txs, ops_storage + : operator_transfer_policy * (transfer list) * operator_storage) : unit = + let validator = make_operator_validator tx_policy in + List.iter (fun (tx : transfer) -> validator (tx.from_, ops_storage)) txs + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_owner_hooks_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_owner_hooks_lib.mligo new file mode 100644 index 0000000..942e019 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_owner_hooks_lib.mligo @@ -0,0 +1,130 @@ +#if !FA2_BEHAVIORS +#define FA2_BEHAVIORS + +(** +Generic implementation of the permission logic for sender and receiver hooks. +Actual behavior is driven by a `permissions_descriptor`. +To be used in FA2 and/or FA2 permission transfer hook contract implementation +which supports sender/receiver hooks. +*) + +#include "../fa2_interface.mligo" +#include "../fa2_errors.mligo" + +type get_owners = transfer_descriptor -> (address option) list + +type hook_result = + | Hook_contract of transfer_descriptor_param_michelson contract + | Hook_undefined of string + +type to_hook = address -> hook_result + +type transfer_hook_params = { + ligo_param : transfer_descriptor_param; + michelson_param : transfer_descriptor_param_michelson; +} + +(** +Extracts a set of unique `from_` or `to_` addresses from the transfer batch. +@param batch transfer batch +@param get_owner selector of `from_` or `to_` addresses from each individual `transfer_descriptor` + *) +let get_owners_from_batch (batch, get_owners : (transfer_descriptor list) * get_owners) : address set = + List.fold + (fun (acc, tx : (address set) * transfer_descriptor) -> + let owners = get_owners tx in + List.fold + (fun (acc, o: (address set) * (address option)) -> + match o with + | None -> acc + | Some a -> Set.add a acc + ) + owners + acc + ) + batch + (Set.empty : address set) + +let validate_owner_hook (p, get_owners, to_hook, is_required : + transfer_hook_params * get_owners * to_hook * bool) + : operation list = + let owners = get_owners_from_batch (p.ligo_param.batch, get_owners) in + Set.fold + (fun (ops, owner : (operation list) * address) -> + match to_hook owner with + | Hook_contract h -> + let op = Operation.transaction p.michelson_param 0mutez h in + op :: ops + | Hook_undefined error -> + (* owner hook is not implemented by the target contract *) + if is_required + then (failwith error : operation list) (* owner hook is required: fail *) + else ops (* owner hook is optional: skip it *) + ) + owners ([] : operation list) + +let validate_owner(p, policy, get_owners, to_hook : + transfer_hook_params * owner_hook_policy * get_owners * to_hook) + : operation list = + match policy with + | Owner_no_hook -> ([] : operation list) + | Optional_owner_hook -> validate_owner_hook (p, get_owners, to_hook, false) + | Required_owner_hook -> validate_owner_hook (p, get_owners, to_hook, true) + +(** +Given an address of the token receiver, tries to get an entry point for +`fa2_token_receiver` interface. + *) +let to_receiver_hook : to_hook = fun (a : address) -> + let c : (transfer_descriptor_param_michelson contract) option = + Operation.get_entrypoint_opt "%tokens_received" a in + match c with + | Some c -> Hook_contract c + | None -> Hook_undefined fa2_receiver_hook_undefined + +(** +Create a list iof Tezos operations invoking all token receiver contracts that +implement `fa2_token_receiver` interface. Fail if specified `owner_hook_policy` +cannot be met. + *) +let validate_receivers (p, receiver_policy : transfer_hook_params * owner_hook_policy) + : operation list = + let get_receivers : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_ )tx.txs in + validate_owner (p, receiver_policy, get_receivers, to_receiver_hook) + +(** +Given an address of the token sender, tries to get an entry point for +`fa2_token_sender` interface. + *) +let to_sender_hook : to_hook = fun (a : address) -> + let c : (transfer_descriptor_param_michelson contract) option = + Operation.get_entrypoint_opt "%tokens_sent" a in + match c with + | Some c -> Hook_contract c + | None -> Hook_undefined fa2_sender_hook_undefined + +(** +Create a list iof Tezos operations invoking all token sender contracts that +implement `fa2_token_sender` interface. Fail if specified `owner_hook_policy` +cannot be met. + *) +let validate_senders (p, sender_policy : transfer_hook_params * owner_hook_policy) + : operation list = + let get_sender : get_owners = fun (tx : transfer_descriptor) -> [tx.from_] in + validate_owner (p, sender_policy, get_sender, to_sender_hook) + +(** +Generate a list of Tezos operations invoking sender and receiver hooks according to +the policies defined by the permissions descriptor. +To be used in FA2 and/or FA2 transfer hook contract implementation which supports +sender/receiver hooks. + *) +let owners_transfer_hook (p, descriptor : transfer_hook_params * permissions_descriptor) + : operation list = + let sender_ops = validate_senders (p, descriptor.sender) in + let receiver_ops = validate_receivers (p, descriptor.receiver) in + (* merge two lists *) + List.fold (fun (l, o : (operation list) * operation) -> o :: l) receiver_ops sender_ops + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.mligo new file mode 100644 index 0000000..a4c99c1 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.mligo @@ -0,0 +1,49 @@ +(** + Helper types and functions to implement transfer hook contract. + Each transfer hook contract maintains a registry of known FA2 contracts and + validates that it is invoked from registered FA2 contracts. + + The implementation assumes that the transfer hook entry point is labeled as + `%tokens_transferred_hook`. + *) + +#if !FA2_HOOK_LIB +#define FA2_HOOK_LIB + +#include "../fa2_hook.mligo" +#include "fa2_convertors.mligo" + +let get_hook_entrypoint (hook_contract : address) (u : unit) + : transfer_descriptor_param_michelson contract = + let hook_entry : transfer_descriptor_param_michelson contract = + Operation.get_entrypoint "%tokens_transferred_hook" hook_contract in + hook_entry + + +let create_register_hook_op + (fa2, descriptor : (fa2_with_hook_entry_points contract) * permissions_descriptor) : operation = + let hook_fn = get_hook_entrypoint Current.self_address in + let p : set_hook_param_aux = { + hook = hook_fn; + permissions_descriptor = permissions_descriptor_to_michelson descriptor; + } in + let pm = Layout.convert_to_right_comb p in + Operation.transaction (Set_transfer_hook pm) 0mutez fa2 + + +type fa2_registry = address set + +let register_with_fa2 (fa2, descriptor, registry : + (fa2_with_hook_entry_points contract) * permissions_descriptor * fa2_registry) + : operation * fa2_registry = + let op = create_register_hook_op (fa2, descriptor) in + let fa2_address = Current.address fa2 in + let new_registry = Set.add fa2_address registry in + op, new_registry + +let validate_hook_call (fa2, registry: address * fa2_registry) : unit = + if Set.mem fa2 registry + then unit + else failwith "UNKNOWN_FA2_CALL" + +#endif From f50c827d18119be2a34ad6b1bdf8098fee5144b4 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Wed, 24 Jun 2020 13:57:52 +0200 Subject: [PATCH 14/16] [Cameligo] Rework/simplify chapter FA2 Operator --- .../src/pages/Chapter/Chapter.data.tsx | 199 +-- .../Chapters/Camel/ChapterFA20/exercise.mligo | 4 +- .../Chapters/Camel/ChapterFA20/solution.mligo | 4 +- .../Camel/ChapterFA20Operator/course.md | 201 +++ .../Camel/ChapterFA20Operator/exercise.cmd | 30 + .../Camel/ChapterFA20Operator/index.ts | 27 + .../Camel/ChapterFA20Operator/solution.cmd | 47 + .../Camel/ChapterFA20Operator/tqtz_nft.mligo | 160 +++ .../tzip-12/.gitattributes | 2 + .../examples/fa2_custom_receiver.mligo | 94 ++ .../tzip-12/examples/fa2_default_hook.mligo | 55 + .../examples/fa2_hook_with_schedule.mligo | 131 ++ .../tzip-12/examples/fa2_operator_lib.mligo | 159 +++ .../tzip-12/fa2_errors.mligo | 49 + .../tzip-12/fa2_hook.mligo | 32 + .../tzip-12/fa2_interface.mligo | 192 +++ .../tzip-12/implementing-fa2.md | 252 ++++ .../tzip-12/lib/fa2_convertors.mligo | 168 +++ .../tzip-12/lib/fa2_operator_lib.mligo | 88 ++ .../tzip-12/lib/fa2_owner_hooks_lib.mligo | 149 +++ .../tzip-12/lib/fa2_transfer_hook_lib.mligo | 49 + .../ChapterFA20Operator/tzip-12/tzip-12.md | 1093 +++++++++++++++++ .../solution_expanded.cmd | 137 +++ 23 files changed, 3246 insertions(+), 76 deletions(-) create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/course.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/exercise.cmd create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/index.ts create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/solution.cmd create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tqtz_nft.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/.gitattributes create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_custom_receiver.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_default_hook.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_hook_with_schedule.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_operator_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_errors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_hook.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_interface.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/implementing-fa2.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_convertors.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_operator_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_owner_hooks_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_transfer_hook_lib.mligo create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/tzip-12.md create mode 100644 src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution_expanded.cmd diff --git a/src/frontend/src/pages/Chapter/Chapter.data.tsx b/src/frontend/src/pages/Chapter/Chapter.data.tsx index 194c32c..6d575fd 100644 --- a/src/frontend/src/pages/Chapter/Chapter.data.tsx +++ b/src/frontend/src/pages/Chapter/Chapter.data.tsx @@ -1,75 +1,91 @@ -import { data as camelDataAddresses } from '../Chapters/Camel/ChapterAddresses' -import { data as camelDataBuiltIns } from '../Chapters/Camel/ChapterBuiltIns' -import { data as camelDataConditionals } from '../Chapters/Camel/ChapterConditionals' -import { data as camelDataDeployContract } from '../Chapters/Camel/ChapterDeployContract' -import { data as camelDataFA12 } from '../Chapters/Camel/ChapterFA12' -import { data as camelDataFunctions } from '../Chapters/Camel/ChapterFunctions' -import { data as camelDataInteractions } from '../Chapters/Camel/ChapterInteractions' -import { data as camelDataLambda } from '../Chapters/Camel/ChapterLambda' -import { data as camelDataLists } from '../Chapters/Camel/ChapterLists' -import { data as camelDataLoops } from '../Chapters/Camel/ChapterLoops' -import { data as camelDataMainFunction } from '../Chapters/Camel/ChapterMainFunction' -import { data as camelDataMaps } from '../Chapters/Camel/ChapterMaps' -import { data as camelDataMath } from '../Chapters/Camel/ChapterMath' -import { data as camelDataMultisig } from '../Chapters/Camel/ChapterMultisig' -import { data as camelDataOption } from '../Chapters/Camel/ChapterOption' -import { data as camelDataPolymorphism } from '../Chapters/Camel/ChapterPolymorphism' -import { data as camelDataRecords } from '../Chapters/Camel/ChapterRecords' -import { data as camelDataStrings } from '../Chapters/Camel/ChapterStrings' -import { data as camelDataTimestamps } from '../Chapters/Camel/ChapterTimestamps' -import { data as camelDataTransactions } from '../Chapters/Camel/ChapterTransactions' -import { data as camelDataTuples } from '../Chapters/Camel/ChapterTuples' -import { data as camelDataTypes } from '../Chapters/Camel/ChapterTypes' -import { data as camelDataVariables } from '../Chapters/Camel/ChapterVariables' -import { data as camelDataVariant } from '../Chapters/Camel/ChapterVariant' -import { data as pascalDataAddresses } from '../Chapters/Pascal/ChapterAddresses' -import { data as pascalDataBuiltIns } from '../Chapters/Pascal/ChapterBuiltIns' -import { data as pascalDataConditionals } from '../Chapters/Pascal/ChapterConditionals' -import { data as pascalDataDeployContract } from '../Chapters/Pascal/ChapterDeployContract' -import { data as pascalDataFA12 } from '../Chapters/Pascal/ChapterFA12' -import { data as pascalDataFunctions } from '../Chapters/Pascal/ChapterFunctions' -import { data as pascalDataInteractions } from '../Chapters/Pascal/ChapterInteractions' -import { data as pascalDataLambda } from '../Chapters/Pascal/ChapterLambda' -import { data as pascalDataLists } from '../Chapters/Pascal/ChapterLists' -import { data as pascalDataLoops } from '../Chapters/Pascal/ChapterLoops' -import { data as pascalDataMainFunction } from '../Chapters/Pascal/ChapterMainFunction' -import { data as pascalDataMaps } from '../Chapters/Pascal/ChapterMaps' -import { data as pascalDataMath } from '../Chapters/Pascal/ChapterMath' -import { data as pascalDataMultisig } from '../Chapters/Pascal/ChapterMultisig' -import { data as pascalDataOption } from '../Chapters/Pascal/ChapterOption' -import { data as pascalDataPolymorphism } from '../Chapters/Pascal/ChapterPolymorphism' -import { data as pascalDataRecords } from '../Chapters/Pascal/ChapterRecords' -import { data as pascalDataStrings } from '../Chapters/Pascal/ChapterStrings' -import { data as pascalDataTimestamps } from '../Chapters/Pascal/ChapterTimestamps' -import { data as pascalDataTransactions } from '../Chapters/Pascal/ChapterTransactions' -import { data as pascalDataTuples } from '../Chapters/Pascal/ChapterTuples' -import { data as pascalDataTypes } from '../Chapters/Pascal/ChapterTypes' -import { data as pascalDataVariables } from '../Chapters/Pascal/ChapterVariables' -import { data as pascalDataVariant } from '../Chapters/Pascal/ChapterVariant' -import { data as reasonDataAddresses } from '../Chapters/Reason/ChapterAddresses' -import { data as reasonDataBuiltIns } from '../Chapters/Reason/ChapterBuiltIns' -import { data as reasonDataConditionals } from '../Chapters/Reason/ChapterConditionals' -import { data as reasonDataDeployContract } from '../Chapters/Reason/ChapterDeployContract' -import { data as reasonDataFA12 } from '../Chapters/Reason/ChapterFA12' -import { data as reasonDataFunctions } from '../Chapters/Reason/ChapterFunctions' -import { data as reasonDataInteractions } from '../Chapters/Reason/ChapterInteractions' -import { data as reasonDataLambda } from '../Chapters/Reason/ChapterLambda' -import { data as reasonDataLists } from '../Chapters/Reason/ChapterLists' -import { data as reasonDataLoops } from '../Chapters/Reason/ChapterLoops' -import { data as reasonDataMainFunction } from '../Chapters/Reason/ChapterMainFunction' -import { data as reasonDataMaps } from '../Chapters/Reason/ChapterMaps' -import { data as reasonDataMath } from '../Chapters/Reason/ChapterMath' -import { data as reasonDataMultisig } from '../Chapters/Reason/ChapterMultisig' -import { data as reasonDataOption } from '../Chapters/Reason/ChapterOption' -import { data as reasonDataPolymorphism } from '../Chapters/Reason/ChapterPolymorphism' -import { data as reasonDataRecords } from '../Chapters/Reason/ChapterRecords' -import { data as reasonDataStrings } from '../Chapters/Reason/ChapterStrings' -import { data as reasonDataTimestamps } from '../Chapters/Reason/ChapterTimestamps' -import { data as reasonDataTransactions } from '../Chapters/Reason/ChapterTransactions' -import { data as reasonDataTuples } from '../Chapters/Reason/ChapterTuples' -import { data as reasonDataTypes } from '../Chapters/Reason/ChapterTypes' -import { data as reasonDataVariables } from '../Chapters/Reason/ChapterVariables' -import { data as reasonDataVariant } from '../Chapters/Reason/ChapterVariant' + +/* prettier-ignore */ + +import { data as pascalDataAddresses } from "../Chapters/Pascal/ChapterAddresses"; +import { data as pascalDataBuiltIns } from "../Chapters/Pascal/ChapterBuiltIns"; +import { data as pascalDataConditionals } from "../Chapters/Pascal/ChapterConditionals"; +import { data as pascalDataDeployContract } from "../Chapters/Pascal/ChapterDeployContract"; +import { data as pascalDataFA12 } from "../Chapters/Pascal/ChapterFA12"; +import { data as pascalDataFunctions } from "../Chapters/Pascal/ChapterFunctions"; +import { data as pascalDataInteractions } from "../Chapters/Pascal/ChapterInteractions"; +import { data as pascalDataInterop } from "../Chapters/Pascal/ChapterInterop"; +import { data as pascalDataLambda } from "../Chapters/Pascal/ChapterLambda"; +import { data as pascalDataLists } from "../Chapters/Pascal/ChapterLists"; +import { data as pascalDataLoops } from "../Chapters/Pascal/ChapterLoops"; +import { data as pascalDataMainFunction } from "../Chapters/Pascal/ChapterMainFunction"; +import { data as pascalDataMaps } from "../Chapters/Pascal/ChapterMaps"; +import { data as pascalDataMath } from "../Chapters/Pascal/ChapterMath"; +import { data as pascalDataMultisig } from "../Chapters/Pascal/ChapterMultisig"; +import { data as pascalDataOption } from "../Chapters/Pascal/ChapterOption"; +import { data as pascalDataPolymorphism } from "../Chapters/Pascal/ChapterPolymorphism"; +import { data as pascalDataRecords } from "../Chapters/Pascal/ChapterRecords"; +import { data as pascalDataStrings } from "../Chapters/Pascal/ChapterStrings"; +import { data as pascalDataTimestamps } from "../Chapters/Pascal/ChapterTimestamps"; +import { data as pascalDataTransactions } from "../Chapters/Pascal/ChapterTransactions"; +import { data as pascalDataTuples } from "../Chapters/Pascal/ChapterTuples"; +import { data as pascalDataTypes } from "../Chapters/Pascal/ChapterTypes"; +import { data as pascalDataVariables } from "../Chapters/Pascal/ChapterVariables"; +import { data as pascalDataVariant } from "../Chapters/Pascal/ChapterVariant"; + +import { data as camelDataAddresses } from "../Chapters/Camel/ChapterAddresses"; +import { data as camelDataBuiltIns } from "../Chapters/Camel/ChapterBuiltIns"; +import { data as camelDataConditionals } from "../Chapters/Camel/ChapterConditionals"; +import { data as camelDataDeployContract } from "../Chapters/Camel/ChapterDeployContract"; +import { data as camelDataFA12 } from "../Chapters/Camel/ChapterFA12"; +import { data as camelFA20 } from "../Chapters/Camel/ChapterFA20"; +import { data as camelFA20Operator } from "../Chapters/Camel/ChapterFA20Operator"; +import { data as camelFA20Hook } from "../Chapters/Camel/ChapterFA20Hook"; +import { data as camelDataFunctions } from "../Chapters/Camel/ChapterFunctions"; +import { data as camelDataInteractions } from "../Chapters/Camel/ChapterInteractions"; +import { data as camelDataInterop } from "../Chapters/Camel/ChapterInterop"; +import { data as camelDataLambda } from "../Chapters/Camel/ChapterLambda"; +import { data as camelDataLists } from "../Chapters/Camel/ChapterLists"; +import { data as camelDataLoops } from "../Chapters/Camel/ChapterLoops"; +import { data as camelDataMainFunction } from "../Chapters/Camel/ChapterMainFunction"; +import { data as camelDataMaps } from "../Chapters/Camel/ChapterMaps"; +import { data as camelDataMath } from "../Chapters/Camel/ChapterMath"; +import { data as camelDataMultisig } from "../Chapters/Camel/ChapterMultisig"; +import { data as camelDataOption } from "../Chapters/Camel/ChapterOption"; +import { data as camelDataPolymorphism } from "../Chapters/Camel/ChapterPolymorphism"; +import { data as camelDataRecords } from "../Chapters/Camel/ChapterRecords"; +import { data as camelDataStrings } from "../Chapters/Camel/ChapterStrings"; +import { data as camelDataTimestamps } from "../Chapters/Camel/ChapterTimestamps"; +import { data as camelDataTransactions } from "../Chapters/Camel/ChapterTransactions"; +import { data as camelDataTuples } from "../Chapters/Camel/ChapterTuples"; +import { data as camelDataTypes } from "../Chapters/Camel/ChapterTypes"; +import { data as camelDataVariables } from "../Chapters/Camel/ChapterVariables"; +import { data as camelDataVariant } from "../Chapters/Camel/ChapterVariant"; + +import { data as reasonDataAddresses } from "../Chapters/Reason/ChapterAddresses"; +import { data as reasonDataBuiltIns } from "../Chapters/Reason/ChapterBuiltIns"; +import { data as reasonDataConditionals } from "../Chapters/Reason/ChapterConditionals"; +import { data as reasonDataDeployContract } from "../Chapters/Reason/ChapterDeployContract"; +import { data as reasonDataFA12 } from "../Chapters/Reason/ChapterFA12"; +import { data as reasonDataFunctions } from "../Chapters/Reason/ChapterFunctions"; +import { data as reasonDataInteractions } from "../Chapters/Reason/ChapterInteractions"; +import { data as reasonDataInterop } from "../Chapters/Reason/ChapterInterop"; +import { data as reasonDataLambda } from "../Chapters/Reason/ChapterLambda"; +import { data as reasonDataLists } from "../Chapters/Reason/ChapterLists"; +import { data as reasonDataLoops } from "../Chapters/Reason/ChapterLoops"; +import { data as reasonDataMainFunction } from "../Chapters/Reason/ChapterMainFunction"; +import { data as reasonDataMaps } from "../Chapters/Reason/ChapterMaps"; +import { data as reasonDataMath } from "../Chapters/Reason/ChapterMath"; +import { data as reasonDataOption } from "../Chapters/Reason/ChapterOption"; +import { data as reasonDataPolymorphism } from "../Chapters/Reason/ChapterPolymorphism"; +import { data as reasonDataRecords } from "../Chapters/Reason/ChapterRecords"; +import { data as reasonDataStrings } from "../Chapters/Reason/ChapterStrings"; +import { data as reasonDataTimestamps } from "../Chapters/Reason/ChapterTimestamps"; +import { data as reasonDataTransactions } from "../Chapters/Reason/ChapterTransactions"; +import { data as reasonDataTuples } from "../Chapters/Reason/ChapterTuples"; +import { data as reasonDataTypes } from "../Chapters/Reason/ChapterTypes"; +import { data as reasonDataVariables } from "../Chapters/Reason/ChapterVariables"; +import { data as reasonDataVariant } from "../Chapters/Reason/ChapterVariant"; + + + + +import { data as reasonDataMultisig } from "../Chapters/Reason/ChapterMultisig"; + export const chapterData = [ { @@ -177,6 +193,13 @@ export const chapterData = [ name: '25 - Pascal - FA12', data: pascalDataFA12, }, + { + pathname: '/pascal/chapter-interop', + language: 'PascaLIGO', + name: '26- Pascal - Interoperability', + data: pascalDataInterop, + }, + { pathname: '/camel/chapter-about', @@ -272,12 +295,37 @@ export const chapterData = [ name: '24 - Camel - Multisignature', data: camelDataMultisig, }, + { + pathname: '/camel/chapter-interop', + language: 'CameLIGO', + name: '25 - Camel - Interoperability', + data: camelDataInterop, + }, + { pathname: '/camel/chapter-fa12', language: 'CameLIGO', name: '25 - Camel - FA12', data: camelDataFA12, }, + { + pathname: "/camel/chapter-fa2", + language: "CameLIGO", + name: "26 - Camel - FA2", + data: camelFA20, + }, + { + pathname: "/camel/chapter-fa2-operator", + language: "CameLIGO", + name: "27 - Camel - FA2 Operator", + data: camelFA20Operator, + }, + { + pathname: "/camel/chapter-fa2-hook", + language: "CameLIGO", + name: "28 - Camel - FA2 Hook", + data: camelFA20Hook, + }, { pathname: '/reason/chapter-about', @@ -394,4 +442,11 @@ export const chapterData = [ name: '25 - Reason - FA12', data: reasonDataFA12, }, + { + pathname: '/reason/chapter-interop', + language: 'ReasonLIGO', + name: '25 - Reason - Interoperability', + data: reasonDataInterop, + }, + ] diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo index 9b61bd0..5f3484f 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/exercise.mligo @@ -64,9 +64,7 @@ let total_supply(params, s: total_supply_param_michelson * storage) : return = let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in let token_ids : token_id list = p.token_ids in // Modify the code below - let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with - | Some v -> { token_id = i ; total_supply =v.total_supply } - | None -> (failwith(token_undefined) : total_supply_response) + let get_total_supply = in let responses : total_supply_response list = List.map get_total_supply token_ids in let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo index 5f3484f..9b61bd0 100644 --- a/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20/solution.mligo @@ -64,7 +64,9 @@ let total_supply(params, s: total_supply_param_michelson * storage) : return = let p : total_supply_param = Layout.convert_from_right_comb(params: total_supply_param_michelson) in let token_ids : token_id list = p.token_ids in // Modify the code below - let get_total_supply = + let get_total_supply = fun ( i : token_id) -> match Map.find_opt i s.tokens with + | Some v -> { token_id = i ; total_supply =v.total_supply } + | None -> (failwith(token_undefined) : total_supply_response) in let responses : total_supply_response list = List.map get_total_supply token_ids in let convert = fun ( r : total_supply_response) -> Layout.convert_to_right_comb(r) in diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/course.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/course.md new file mode 100644 index 0000000..062f58c --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/course.md @@ -0,0 +1,201 @@ +# Chapter 24 : Financial Asset 2.0 - Operators and Permissions + +Captain, why are you trying to change the part yourself? Just write a function on the terminal and send it to a droid. + +## Definition + +The FA2 standard proposes a *unified token contract interface* that accommodates all mentioned concerns. It aims to provide significant expressivity to contract developers to create new types of tokens while maintaining a common interface standard for wallet integrators and external developers. + +In this chapter we will focus on _Operators_ and _Permissions_. + +## Entry points + +Token contract implementing the FA2 standard MUST have the following entry points. + +``` +type fa2_entry_points = + +| Transfer of transfer list +| Balance_of of balance_of_param +| Total_supply of total_supply_param +| Token_metadata of token_metadata_param +| Permissions_descriptor of permissions_descriptor contract +| Update_operators of update_operator list +| Is_operator of is_operator_param +``` + +### Operators + +#### Definition +_Operator_ can be seen as delegate role. + +_Operator_ is a Tezos address that initiates token transfer operation on behalf of the owner. +_Owner_ is a Tezos address which can hold tokens. +An operator, other than the owner, MUST be approved to manage particular token types held by the owner to make a transfer from the owner account. +Operator relation is not transitive. If C is an operator of B , and if B is an operator of A, C cannot transfer tokens that are owned by A, on behalf of B. + +an _operator_ is defined as a relationship between two address (owner address and operator address) and can be understood as an operator address who can operate tokens held by a owner. + +#### FA2 interface operator + +FA2 interface specifies two entry points to update and inspect operators. Once permitted for the specific token owner, an operator can transfer any tokens belonging to the owner. + +``` +| Update_operators of update_operator_michelson list +| Is_operator of is_operator_param +``` + +where parameter type *update_operator* and *is_operator_param* are : +``` +type operator_param = { + owner : address; + operator : address; +} + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type is_operator_param = { + operator : operator_param; + callback : (is_operator_response_michelson) contract; +} +``` + +Notice the *update_operator* can only Add or Remove an _operator_ (an allowance between an operator address and a token owner address). + +Notice the parameter _is_operator_param_ given to *Is_operator* entry point contains a *callback* property used to send back a response to the calling contract. + +Notice entry point *Update_operators* expectes a list of *update_operator_michelson*. The fa2 convertor helper provide the *operator_param_to_michelson* function to convert *operator_param* format into *update_operator_michelson* format. + + +#### FA2 standard operator library + +Some helpers functions has been implemented in the FA2 library which help manipulating _operator_. This library contains following functions and type alias : + + +an _operator_ is a relationship between two address (owner address and operator address) + +function *is_operator* returns to a contract caller whether an operator address is associated to an owner address + +function *update_operators* allows to Add or Remove an operator in the list of operators. + +function *validate_update_operators_by_owner*, it ensures the given adress is owner of an _operator_ + +the function *validate_operator* validates operators for all transfers in the batch at once, depending on given operator_transfer_policy + + + +### FA2 Permission Policies and Configuration + +Most token standards specify logic that validates a transfer transaction and can either approve or reject a transfer. +Such logic (called _Permission Policy_) could validate who initiates a transfer, the transfer amount, and who can receive tokens. + +This FA2 standard defines a framework to compose and configure such permission policies from the standard behaviors and configuration APIs. + +FA2 defines : +* the default core transfer behavior, that MUST always be implemented +* a set of predefined permission policies that are optional + + +#### permissions_descriptor + +FA2 specifies an interface permissions_descriptor allowing external contracts to discover an FA2 contract's permission policy and to configure it. *permissions_descriptor* serves as a modular approach to define consistent and non-self-contradictory policies. + +The *permission descriptor* indicates which standard permission policies are implemented by the FA2 contract and can be used by off-chain and on-chain tools to discover the properties of the particular FA2 contract implementation. + +The FA2 standard defines a special metadata entry point *permission descriptor* containing standard permission policies. +``` +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} +``` + + +#### operator_transfer_policy + +operator_transfer_policy - defines who can transfer tokens. Tokens can be +transferred by the token owner or an operator (some address that is authorized to +transfer tokens on behalf of the token owner). A special case is when neither owner +nor operator can transfer tokens (can be used for non-transferable tokens). The +FA2 standard defines two entry points to manage and inspect operators associated +with the token owner address (*update_operators*, +*is_operator*). Once an operator is added, it can manage all of +its associated owner's tokens. + +``` +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer +``` + +#### owner_hook_policy + +owner_hook_policy - defines if sender/receiver hooks should be called or +not. Each token owner contract MAY implement either an *fa2_token_sender* or +*fa2_token_receiver* hook interface. Those hooks MAY be called when a transfer sends +tokens from the owner account or the owner receives tokens. The hook can either +accept a transfer transaction or reject it by failing. + +``` +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook +``` + +#### custom_permission_policy + +It is possible to extend permission policy with a custom behavior, which does +not overlap with already existing standard policies. This standard does not specify +exact types for custom config entry points. FA2 token contract clients that support +custom config entry points must know their types a priori and/or use a tag hint +of custom_permission_policy. + +``` +type custom_permission_policy = { + tag : string; + config_api: address option; +} +``` + + +#### Permission Policy Formulae + +Each concrete implementation of the permission policy can be described by a formula which combines permission behaviors in the following form: +``` +Operator(?) * Receiver(?) * Sender(?) +``` + +This formula describes the policy which allows only token owners to transfer their own +tokens : +``` +Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook) +``` + + + + + +## Your mission + +We are working on a non_fungible/single-asset token. +Our NFT "token" is almost ready but to allow a new rule. We need Bob to transfert a token taken from Vera account and send it to Alice account. + + * Alice's account address is "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" + * Bob's account address is "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU" + * Vera's account address is "tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv" + +1- First we want you to prepare the initial state of storage. Modify the _ligo compile-storage_ command for the *token* contract with following recommandations : + + * Vera account is owner of the token 1 + +2- Complete the _ligo dry-run_ command for authorizing Bob to transfer token taken from Vera account, transaction emitted by Vera. (reuse the storage you made on step 1). You can use *operator_update_to_michelson* function to convert your parameters into the format expected by *Update_operators* entry point. + + +3- Complete the _ligo dry-run_ command for simulating the transfer of 1 token from Vera'account to Alice's account, transaction emitted by Bob. The transfered token id is number 1. You can use the *transfer_to_michelson* function to convert your parameters into the format expected by *Transfer* entry point. +You will have to modify the storage to in the state where "Vera account is owner of the token 1" (step 1) and Bob is authorized to transfer token taken from Vera account (step 2). diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/exercise.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/exercise.cmd new file mode 100644 index 0000000..5fa8aa6 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/exercise.cmd @@ -0,0 +1,30 @@ +ligo compile-storage tqtz_nft.mligo nft_token_main +'{ + ledger = (Big_map.empty : (token_id, address) big_map); + operators = (Big_map.empty : ((address * address), unit) big_map); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' + +ligo dry-run --sender=tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv tqtz_nft.mligo nft_token_main +'Fa2 (Update_operators([ + +]))' +'' + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU tqtz_nft.mligo nft_token_main +'Fa2 (Transfer( [ + +]))' +'{ + ledger = (Big_map.empty : (token_id, address) big_map); + operators = (Big_map.empty : ((address * address), unit) big_map); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/index.ts b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/index.ts new file mode 100644 index 0000000..77e1835 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/index.ts @@ -0,0 +1,27 @@ +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import course from "!raw-loader!./course.md"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import exercise from "!raw-loader!./exercise.cmd"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import solution from "!raw-loader!./solution.cmd"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support1 from "!raw-loader!./tzip-12/lib/fa2_convertors.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support2 from "!raw-loader!./tzip-12/lib/fa2_operator_lib.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support4 from "!raw-loader!./tzip-12/fa2_interface.mligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support5 from "!raw-loader!./tzip-12/fa2_errors.mligo"; + +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support6 from "!raw-loader!./tqtz_nft.mligo"; + +export const data = { course, exercise, solution, support1, support2, support4, support5, support6 }; diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/solution.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/solution.cmd new file mode 100644 index 0000000..3501d2f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/solution.cmd @@ -0,0 +1,47 @@ +ligo compile-storage tqtz_nft.mligo nft_token_main +'{ + ledger = Big_map.literal([(1n,("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]); + operators = (Big_map.empty : ((address * address), unit) big_map); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' + +ligo dry-run --sender=tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv tqtz_nft.mligo nft_token_main +'Fa2 (Update_operators([ + operator_update_to_michelson (Add_operator_p({ + owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address) + })) +]))' +'{ + ledger = Big_map.literal([(1n,("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]); + operators = (Big_map.empty : ((address * address), unit) big_map); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU tqtz_nft.mligo nft_token_main +'Fa2 (Transfer( [ + transfer_to_michelson ({ + from_=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + txs=[{ + to_=("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address); + token_id=1n; + amount=1n}] + }) +]))' +'{ + ledger = Big_map.literal([(1n,("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]); + operators = Big_map.literal([ ((("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv": address), ("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address)), unit) ]); + metadata = { + token_defs = Set.add {from_=1n; to_=1000n} (Set.empty : token_def set); + last_used_id = 1n; + metadata = Big_map.literal([ ({from_=1n;to_=1000n},{token_id=0n; symbol="<3"; name="TzAcademyShip"; decimals=0n; extras=(Map.empty :(string, string) map)}) ]) + } +}' diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tqtz_nft.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tqtz_nft.mligo new file mode 100644 index 0000000..f2e661f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tqtz_nft.mligo @@ -0,0 +1,160 @@ +(** +Implementation of the FA2 interface for the NFT contract supporting multiple +types of NFTs. Each NFT type is represented by the range of token IDs - `token_def`. + *) +#if !FA2_NFT_TOKEN +#define FA2_NFT_TOKEN + +#include "tzip-12/fa2_interface.mligo" +#include "tzip-12/fa2_errors.mligo" +#include "tzip-12/lib/fa2_operator_lib.mligo" + +(* range of nft tokens *) +type token_def = { + from_ : nat; + to_ : nat; +} + +type nft_meta = (token_def, token_metadata) big_map + +type token_storage = { + token_defs : token_def set; + last_used_id : token_id; + metadata : nft_meta; +} + +type ledger = (token_id, address) big_map + +type nft_token_storage = { + ledger : ledger; + operators : operator_storage; + metadata : token_storage; +} + +(** +Retrieve the balances for the specified tokens and owners +@return callback operation +*) +let get_balance (p, ledger : balance_of_param * ledger) : operation = + let to_balance = fun (r : balance_of_request) -> + let owner = Big_map.find_opt r.token_id ledger in + let response = match owner with + | None -> (failwith fa2_token_undefined : balance_of_response) + | Some o -> + let bal = if o = r.owner then 1n else 0n in + { request = r; balance = bal; } + in + balance_of_response_to_michelson response + in + let responses = List.map to_balance p.requests in + Operation.transaction responses 0mutez p.callback + +(** +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 owner_validator function that validates of the tokens from the particular owner can be transferred. + *) +let transfer (txs, owner_validator, ops_storage, ledger + : (transfer list) * ((address * operator_storage) -> unit) * operator_storage * ledger) : ledger = + (* process individual transfer *) + let make_transfer = (fun (l, tx : ledger * transfer) -> + let u = owner_validator (tx.from_, ops_storage) in + List.fold + (fun (ll, dst : ledger * transfer_destination) -> + if dst.amount = 0n + then ll + else if dst.amount <> 1n + then (failwith fa2_insufficient_balance : ledger) + else + let owner = Big_map.find_opt dst.token_id ll in + match owner with + | None -> (failwith fa2_token_undefined : ledger) + | Some o -> + if o <> tx.from_ + then (failwith fa2_insufficient_balance : ledger) + else Big_map.update dst.token_id (Some dst.to_) ll + ) tx.txs l + ) + in + + List.fold make_transfer txs ledger + +(** Finds a definition of the token type (token_id range) associated with the provided token id *) +let find_token_def (tid, token_defs : token_id * (token_def set)) : token_def = + let tdef = Set.fold (fun (res, d : (token_def option) * token_def) -> + match res with + | Some r -> res + | None -> + if tid >= d.from_ && tid < d.to_ + then Some d + else (None : token_def option) + ) token_defs (None : token_def option) + in + match tdef with + | None -> (failwith fa2_token_undefined : token_def) + | Some d -> d + +let get_metadata (tokens, meta : (token_id list) * token_storage ) + : token_metadata list = + List.map (fun (tid: token_id) -> + let tdef = find_token_def (tid, meta.token_defs) in + let meta = Big_map.find_opt tdef meta.metadata in + match meta with + | Some m -> { m with token_id = tid; } + | None -> (failwith "NO_DATA" : token_metadata) + ) tokens + +let fa2_main (param, storage : fa2_entry_points * nft_token_storage) + : (operation list) * nft_token_storage = + match param with + | Transfer txs_michelson -> + let txs = transfers_from_michelson txs_michelson in + let validator = make_default_operator_validator Tezos.sender in + let new_ledger = transfer (txs, validator, storage.operators, storage.ledger) in + let new_storage = { storage with ledger = new_ledger; } + in ([] : operation list), new_storage + + | Balance_of pm -> + let p = balance_of_param_from_michelson pm in + let op = get_balance (p, storage.ledger) in + [op], storage + + | Update_operators updates_michelson -> + let updates = operator_updates_from_michelson updates_michelson in + let updater = Tezos.sender in + let process_update = (fun (ops, update : operator_storage * update_operator) -> + let u = validate_update_operators_by_owner (update, updater) in + update_operators (update, ops) + ) in + let new_ops = + List.fold process_update updates storage.operators in + let new_storage = { storage with operators = new_ops; } in + ([] : operation list), new_storage + + | Token_metadata_registry callback -> + (* the contract stores its own token metadata and exposes `token_metadata` entry point *) + let callback_op = Operation.transaction Tezos.self_address 0mutez callback in + [callback_op], storage + + + + type nft_entry_points = + | Fa2 of fa2_entry_points + | Metadata of fa2_token_metadata + + let nft_token_main (param, storage : nft_entry_points * nft_token_storage) + : (operation list) * nft_token_storage = + match param with + | Fa2 fa2 -> fa2_main (fa2, storage) + | Metadata m -> ( match m with + | Token_metadata pm -> + let p : token_metadata_param = Layout.convert_from_right_comb pm in + let metas = get_metadata (p.token_ids, storage.metadata) in + let metas_michelson = token_metas_to_michelson metas in + let u = p.handler metas_michelson in + ([] : operation list), storage + ) + + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/.gitattributes b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/.gitattributes new file mode 100644 index 0000000..e2bd9c3 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/.gitattributes @@ -0,0 +1,2 @@ +*.mligo gitlab-language=ocaml +*.religo gitlab-language=ocaml \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_custom_receiver.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_custom_receiver.mligo new file mode 100644 index 0000000..e14dd61 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_custom_receiver.mligo @@ -0,0 +1,94 @@ +(** +Implementation of the permission transfer hook, with custom behavior. +It uses a combination of a receiver while list and `fa2_token_receiver` interface. +Transfer is permitted if a receiver address is in the receiver white list OR implements +`fa2_token_receiver` interface. If a receiver address implements `fa2_token_receiver` +interface, its `tokens_received` entry point must be called. +*) + +#include "../lib/fa2_transfer_hook_lib.mligo" +#include "../lib/fa2_owner_hooks_lib.mligo" + + +type storage = { + fa2_registry : fa2_registry; + receiver_whitelist : address set; +} + +let custom_validate_receivers (p, pm, wl + : transfer_descriptor_param * transfer_descriptor_param_michelson * (address set)) + : operation list = + let get_receiver : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_) tx.txs in + let receivers = get_owners_from_batch (p.batch, get_receiver) in + Set.fold + (fun (ops, r : (operation list) * address) -> + match to_receiver_hook r with + | Hook_entry_point h -> + (* receiver contract implements fa2_token_receiver interface: invoke it*) + let op = Operation.transaction pm 0mutez h in + op :: ops + | Hook_undefined err -> + (* receiver contract does not implement fa2_token_receiver interface: check whitelist*) + if Set.mem r wl + then ops + else (failwith err : operation list) + ) + receivers ([] : operation list) + +let custom_transfer_hook (p, pm, s + : transfer_descriptor_param * transfer_descriptor_param_michelson * storage) : operation list = + custom_validate_receivers (p, pm, s.receiver_whitelist) + + +let get_policy_descriptor (u : unit) : permissions_descriptor = + { + operator = Owner_or_operator_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook ; (* overridden by the custom policy *) + custom = Some { + tag = "receiver_hook_and_whitelist"; + config_api = (Some Current.self_address); + }; + } + +type config_whitelist = + | Add_receiver_to_whitelist of address set + | Remove_receiver_from_whitelist of address set + +let configure_receiver_whitelist (cfg, wl : config_whitelist * (address set)) + : address set = + match cfg with + | Add_receiver_to_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.add a l) + rs wl + | Remove_receiver_from_whitelist rs -> + Set.fold + (fun (l, a : (address set) * address) -> Set.remove a l) + rs wl + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param_michelson + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_receiver_whitelist of config_whitelist + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook pm -> + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let p = transfer_descriptor_param_from_michelson pm in + let ops = custom_transfer_hook (p, pm, s) in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = get_policy_descriptor unit in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_receiver_whitelist cfg -> + let new_wl = configure_receiver_whitelist (cfg, s.receiver_whitelist) in + let new_s = { s with receiver_whitelist = new_wl; } in + ([] : operation list), new_s diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_default_hook.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_default_hook.mligo new file mode 100644 index 0000000..ff006d8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_default_hook.mligo @@ -0,0 +1,55 @@ +(** +Implementation of a generic permission transfer hook that supports sender/receiver +hooks. Contract behavior is driven by the permissions descriptor value in the +contract storage and its particular settings for `sender` and `receiver` policies. +*) + +#include "../lib/fa2_transfer_hook_lib.mligo" +#include "../lib/fa2_owner_hooks_lib.mligo" + +type storage = { + fa2_registry : fa2_registry; + descriptor : permissions_descriptor; +} + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param_michelson + | Register_with_fa2 of fa2_with_hook_entry_points contract + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook pm -> + let p = transfer_descriptor_param_from_michelson pm in + let u = validate_hook_call (Tezos.sender, s.fa2_registry) in + let hook_calls = owners_transfer_hook (p, s.descriptor) in + let ops = List.map (fun (call : hook_entry_point) -> + Operation.transaction pm 0mutez call + ) hook_calls + in + ops, s + + | Register_with_fa2 fa2 -> + let op , new_registry = register_with_fa2 (fa2, s.descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + + +(** example policies *) + +(* the policy which allows only token owners to transfer their own tokens. *) +let own_policy : permissions_descriptor = { + operator = Owner_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook; + custom = (None : custom_permission_policy option); +} + +(* non-transferable token (neither token owner, nor operators can transfer tokens. *) + let own_policy : permissions_descriptor = { + operator = No_transfer; + sender = Owner_no_hook; + receiver = Owner_no_hook; + custom = (None : custom_permission_policy option); +} diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_hook_with_schedule.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_hook_with_schedule.mligo new file mode 100644 index 0000000..5c7cc89 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_hook_with_schedule.mligo @@ -0,0 +1,131 @@ +(** +Implementation of a generic permission transfer hook that supports sender/receiver +hooks. Contract behavior is driven by the permissions descriptor value in the +contract storage and its particular settings for `sender` and `receiver` policies. + +It is possible to use additional custom policy "schedule" which let pause/unpause +transfers based on used schedule +*) + +#include "../lib/fa2_transfer_hook_lib.mligo" +#include "../lib/fa2_owner_hooks_lib.mligo" + +type schedule_interval = { + interval : int; + locked : bool; +} + +type schedule = { + start : timestamp; + schedule : schedule_interval list; + cyclic : bool; +} + +type schedule_policy = { + schedule : schedule; + schedule_interval : int; +} + +type permission_policy = { + descriptor : permissions_descriptor; + schedule_policy : schedule_policy option; +} + +type storage = { + fa2_registry : fa2_registry; + policy : permission_policy; +} + +type schedule_config = + | Set_schedule of schedule + | View_schedule of (schedule option) contract + +let configure_schedule (cfg, policy : schedule_config * schedule_policy option) + : (operation list) * (schedule_policy option) = + match cfg with + | Set_schedule s -> + let total_interval = List.fold + (fun (t, i : int * schedule_interval) -> t + i.interval) + s.schedule 0 in + let new_policy : schedule_policy = { schedule = s; schedule_interval = total_interval; } in + ([] : operation list), (Some new_policy) + | View_schedule v -> + let s = match policy with + | Some p -> Some p.schedule + | None -> (None : schedule option) + in + let op = Operation.transaction s 0mutez v in + [op], policy + +let custom_policy_to_descriptor (p : permission_policy) : permissions_descriptor = + match p.schedule_policy with + | None -> p.descriptor + | Some s -> + let custom_p : custom_permission_policy = { + tag = "schedule"; + config_api = Some Current.self_address; + } + in + {p.descriptor with custom = Some custom_p; } + +type interval_result = + | Reminder of int + | Found of schedule_interval + +let is_schedule_locked (policy : schedule_policy) : bool = + let elapsed : int = Current.time - policy.schedule.start in + if elapsed > policy.schedule_interval && not policy.schedule.cyclic + then true + else (* find schedule interval *) + let e = (elapsed mod policy.schedule_interval) + 0 in + let interval = List.fold + (fun (acc, i : interval_result * schedule_interval) -> + match acc with + | Found si -> acc + | Reminder r -> + if r < i.interval then Found i + else Reminder (r - i.interval) + ) policy.schedule.schedule (Reminder e) in + match interval with + | Reminder r -> (failwith "SCHEDULE_ERROR" : bool) + | Found i -> i.locked + +let validate_schedule (policy : schedule_policy option) : unit = + match policy with + | None -> unit + | Some p -> + let locked = is_schedule_locked p in + if locked + then failwith "SCHEDULE_LOCKED" + else unit + +type entry_points = + | Tokens_transferred_hook of transfer_descriptor_param_michelson + | Register_with_fa2 of fa2_with_hook_entry_points contract + | Config_schedule of schedule_config + + let main (param, s : entry_points * storage) + : (operation list) * storage = + match param with + | Tokens_transferred_hook pm -> + let p = transfer_descriptor_param_from_michelson pm in + let u1 = validate_hook_call (Tezos.sender, s.fa2_registry) in + let u2 = validate_schedule(s.policy.schedule_policy) in + let hook_calls = owners_transfer_hook(p, s.policy.descriptor) in + let ops = List.map (fun (call : hook_entry_point) -> + Operation.transaction pm 0mutez call + ) hook_calls + in + ops, s + + | Register_with_fa2 fa2 -> + let descriptor = custom_policy_to_descriptor s.policy in + let op , new_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry) in + let new_s = { s with fa2_registry = new_registry; } in + [op], new_s + + | Config_schedule cfg -> + let ops, new_schedule = configure_schedule (cfg, s.policy.schedule_policy) in + let new_s = { s with policy.schedule_policy = new_schedule; } in + ops, new_s + diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_operator_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_operator_lib.mligo new file mode 100644 index 0000000..d0fc331 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/examples/fa2_operator_lib.mligo @@ -0,0 +1,159 @@ +(** Reference implementation of the FA2 operator storage and config API functions *) + +#include "../fa2_interface.mligo" + +type operator_tokens_entry = + | All_operator_tokens + | Some_operator_tokens of token_id set + | All_operator_tokens_except of token_id set + +(* (owner * operator) -> tokens *) +type operator_storage = ((address * address), operator_tokens_entry) big_map + +let add_tokens (existing_ts, ts_to_add : (operator_tokens_entry option) * (token_id set)) + : operator_tokens_entry = + match existing_ts with + | None -> Some_operator_tokens ts_to_add + | Some ets -> ( + match ets with + | All_operator_tokens -> All_operator_tokens + | Some_operator_tokens ets -> + (* merge sets *) + let new_ts = Set.fold + (fun (acc, tid : (token_id set) * token_id) -> Set.add tid acc) + ts_to_add ets in + Some_operator_tokens new_ts + | All_operator_tokens_except ets -> + (* subtract sets *) + let new_ts = Set.fold + (fun (acc, tid : (token_id set) * token_id) -> Set.remove tid acc) + ts_to_add ets in + if (Set.size new_ts) = 0n + then All_operator_tokens + else All_operator_tokens_except new_ts + ) + +let add_operator (op, storage : operator_param * operator_storage) : operator_storage = + let key = op.owner, op.operator in + let new_tokens = match op.tokens with + | All_tokens -> All_operator_tokens + | Some_tokens ts_to_add -> + let existing_tokens = Big_map.find_opt key storage in + add_tokens (existing_tokens, ts_to_add) + in + Big_map.update key (Some new_tokens) storage + +let remove_tokens (existing_ts, ts_to_remove : (operator_tokens_entry option) * (token_id set)) + : operator_tokens_entry option = + match existing_ts with + | None -> (None : operator_tokens_entry option) + | Some ets -> ( + match ets with + | All_operator_tokens -> Some (All_operator_tokens_except ts_to_remove) + | Some_operator_tokens ets -> + (* subtract sets *) + let new_ts = Set.fold + (fun (acc, tid : (token_id set) * token_id) -> Set.remove tid acc) + ts_to_remove ets in + if (Set.size new_ts) = 0n + then (None : operator_tokens_entry option) + else Some (Some_operator_tokens new_ts) + | All_operator_tokens_except ets -> + (* merge sets *) + let new_ts = Set.fold + (fun (acc, tid : (token_id set) * token_id) -> Set.add tid acc) + ts_to_remove ets in + Some (All_operator_tokens_except new_ts) + ) + +let remove_operator (op, storage : operator_param * operator_storage) : operator_storage = + let key = op.owner, op.operator in + let new_tokens_opt = match op.tokens with + | All_tokens -> (None : operator_tokens_entry option) + | Some_tokens ts_to_remove -> + let existing_tokens = Big_map.find_opt key storage in + remove_tokens (existing_tokens, ts_to_remove) + in + Big_map.update key new_tokens_opt storage + +let are_tokens_included (existing_tokens, ts : operator_tokens_entry * operator_tokens) : bool = + match existing_tokens with + | All_operator_tokens -> true + | Some_operator_tokens ets -> ( + match ts with + | All_tokens -> false + | Some_tokens ots -> + (* all ots tokens must be in ets set*) + Set.fold (fun (res, ti : bool * token_id) -> + if (Set.mem ti ets) then res else false + ) ots true + ) + | All_operator_tokens_except ets -> ( + match ts with + | All_tokens -> false + | Some_tokens ots -> + (* None of the its tokens must be in ets *) + Set.fold (fun (res, ti : bool * token_id) -> + if (Set.mem ti ets) then false else res + ) ots true + ) + +let is_operator_impl (p, storage : operator_param * operator_storage) : bool = + let key = p.owner, p.operator in + let op_tokens = Big_map.find_opt key storage in + match op_tokens with + | None -> false + | Some existing_tokens -> are_tokens_included (existing_tokens, p.tokens) + +let update_operators (params, storage : (update_operator list) * operator_storage) + : operator_storage = + List.fold + (fun (s, up : operator_storage * update_operator) -> + match up with + | Add_operator op -> add_operator (op, s) + | Remove_operator op -> remove_operator (op, s) + ) params storage + +let is_operator (param, storage : is_operator_param * operator_storage) : operation = + let is_op = is_operator_impl (param.operator, storage) in + let r : is_operator_response = { + operator = param.operator; + is_operator = is_op; + } in + Operation.transaction r 0mutez param.callback + +type owner_to_tokens = (address, (token_id set)) map + +let validate_operator (self, txs, ops_storage + : self_transfer_policy * (transfer list) * operator_storage) : unit = + let can_self_tx = match self with + | Self_transfer_permitted -> true + | Self_transfer_denied -> false + in + let operator = Current.sender in + let tokens_by_owner = List.fold + (fun (owners, tx : owner_to_tokens * transfer) -> + let tokens = Map.find_opt tx.from_ owners in + let new_tokens = match tokens with + | None -> Set.literal [tx.token_id] + | Some ts -> Set.add tx.token_id ts + in + Map.update tx.from_ (Some new_tokens) owners + ) txs (Map.empty : owner_to_tokens) in + + Map.iter + (fun (owner, tokens : address * (token_id set)) -> + if can_self_tx && owner = operator + then unit + else + let oparam : operator_param = { + owner = owner; + operator = sender; + tokens = Some_tokens tokens; + } in + let is_op = is_operator_impl (oparam, ops_storage) in + if is_op then unit else failwith "not permitted operator" + ) tokens_by_owner + + +let test(u : unit) = unit \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_errors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_errors.mligo new file mode 100644 index 0000000..35a8ab8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_errors.mligo @@ -0,0 +1,49 @@ +#if !FA2_ERRORS +#define FA2_ERRORS + +(** One of the specified `token_id`s is not defined within the FA2 contract *) +let fa2_token_undefined = "FA2_TOKEN_UNDEFINED" +(** +A token owner does not have sufficient balance to transfer tokens from +owner's account +*) +let fa2_insufficient_balance = "FA2_INSUFFICIENT_BALANCE" +(** A transfer failed because of `operator_transfer_policy == No_transfer` *) +let fa2_tx_denied = "FA2_TX_DENIED" +(** +A transfer failed because `operator_transfer_policy == Owner_transfer` and it is +initiated not by the token owner +*) +let fa2_not_owner = "FA2_NOT_OWNER" +(** +A transfer failed because `operator_transfer_policy == Owner_or_operator_transfer` +and it is initiated neither by the token owner nor a permitted operator + *) +let fa2_not_operator = "FA2_NOT_OPERATOR" +(** +`update_operators` entry point is invoked and `operator_transfer_policy` is +`No_transfer` or `Owner_transfer` +*) +let fa2_operators_not_supported = "FA2_OPERATORS_UNSUPPORTED" +(** +Receiver hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let fa2_receiver_hook_failed = "FA2_RECEIVER_HOOK_FAILED" +(** +Sender hook is invoked and failed. This error MUST be raised by the hook +implementation + *) +let fa2_sender_hook_failed = "FA2_SENDER_HOOK_FAILED" +(** +Receiver hook is required by the permission behavior, but is not implemented by +a receiver contract + *) +let fa2_receiver_hook_undefined = "FA2_RECEIVER_HOOK_UNDEFINED" +(** +Sender hook is required by the permission behavior, but is not implemented by +a sender contract + *) +let fa2_sender_hook_undefined = "FA2_SENDER_HOOK_UNDEFINED" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_hook.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_hook.mligo new file mode 100644 index 0000000..f8dd01f --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_hook.mligo @@ -0,0 +1,32 @@ +(** +Optional FA2 contract entry point to setup a transfer hook contract. +Transfer hook is one recommended design pattern to implement FA2 that enables +separation of the core token transfer logic and a permission policy. Instead of +implementing FA2 as a monolithic contract, a permission policy can be implemented +as a separate contract. Permission policy contract provides an entry point invoked +by the core FA2 contract to accept or reject a particular transfer operation (such +an entry point is called transfer hook) + *) + +#if !FA2_HOOK +#define FA2_HOOK + +#include "fa2_interface.mligo" + + +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb + +type fa2_with_hook_entry_points = + | Set_transfer_hook of set_hook_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_interface.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_interface.mligo new file mode 100644 index 0000000..4245064 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/fa2_interface.mligo @@ -0,0 +1,192 @@ +#if ! FA2_INTERFACE +#define FA2_INTERFACE + +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + +type transfer_michelson = transfer_aux michelson_pair_right_comb + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_request_michelson = balance_of_request michelson_pair_right_comb + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_response_aux = { + request : balance_of_request_michelson; + balance : nat; +} + +type balance_of_response_michelson = balance_of_response_aux michelson_pair_right_comb + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_aux = { + requests : balance_of_request_michelson list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_michelson = balance_of_param_aux michelson_pair_right_comb + +type operator_param = { + owner : address; + operator : address; +} + +type operator_param_michelson = operator_param michelson_pair_right_comb + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +type update_operator_aux = + | Add_operator of operator_param_michelson + | Remove_operator of operator_param_michelson + +type update_operator_michelson = update_operator_aux michelson_or_right_comb + +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} + +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param = { + token_ids : token_id list; + handler : (token_metadata_michelson list) -> unit; +} + +type token_metadata_param_michelson = token_metadata_param michelson_pair_right_comb + +type fa2_entry_points = + | Transfer of transfer_michelson list + | Balance_of of balance_of_param_michelson + | Update_operators of update_operator_michelson list + | Token_metadata_registry of address contract + + +type fa2_token_metadata = + | Token_metadata of token_metadata_param_michelson + +(* permission policy definition *) + +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer + +type operator_transfer_policy_michelson = operator_transfer_policy michelson_or_right_comb + +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook + +type owner_hook_policy_michelson = owner_hook_policy michelson_or_right_comb + +type custom_permission_policy = { + tag : string; + config_api: address option; +} + +type custom_permission_policy_michelson = custom_permission_policy michelson_pair_right_comb + +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} + +type permissions_descriptor_aux = { + operator : operator_transfer_policy_michelson; + receiver : owner_hook_policy_michelson; + sender : owner_hook_policy_michelson; + custom : custom_permission_policy_michelson option; +} + +type permissions_descriptor_michelson = permissions_descriptor_aux michelson_pair_right_comb + +(* permissions descriptor entry point +type fa2_entry_points_custom = + ... + | Permissions_descriptor of permissions_descriptor_michelson contract + +*) + + +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param = { + batch : transfer_descriptor list; + operator : address; +} + +type transfer_descriptor_param_aux = { + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb +(* +Entry points for sender/receiver hooks + +type fa2_token_receiver = + ... + | Tokens_received of transfer_descriptor_param_michelson + +type fa2_token_sender = + ... + | Tokens_sent of transfer_descriptor_param_michelson +*) + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/implementing-fa2.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/implementing-fa2.md new file mode 100644 index 0000000..9ae0fa0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/implementing-fa2.md @@ -0,0 +1,252 @@ + +# Implementing FA2 + +## Transfer Hook + +Transfer hook is one recommended design pattern to implement FA2 that enables +separation of the core token transfer logic and a permission policy. Instead of +implementing FA2 as a monolithic contract, a [permission policy] +(#fa2-permission-policies-and-configuration) can be implemented as a separate +contract. Permission policy contract provides an entry point invoked by the core +FA2 contract to accept or reject a particular transfer operation (such +an entry point is called **transfer hook**). + +### Transfer Hook Motivation + +Usually, different tokens require different permission policies that define who +can transfer and receive tokens. There is no single permission policy that fits +all scenarios. For instance, some game tokens can be transferred by token owners, +but no one else. In some financial token exchange applications, tokens are to be +transferred by a special exchange operator account, not directly by the token owners +themselves. + +Support for different permission policies usually requires customizing existing +contract code. The FA2 standard proposes a different approach in which the on-chain +composition of the core FA2 contract implementation does not change, and a pluggable +permission transfer hook is implemented as a separate contract and registered with +the core FA2. Every time FA2 performs a transfer, it invokes a hook contract that +validates a transaction and either approves it by finishing execution successfully +or rejects it by failing. + +The transfer hook makes it possible to model different transfer permission +policies like whitelists, operator lists, etc. Although this approach introduces +gas consumption overhead (compared to an all-in-one contract) by requiring an extra +inter-contract call, it also offers some other advantages: + +* FA2 core implementation can be verified once, and certain properties (not + related to permission policy) remain unchanged. + +* Most likely, the core transfer semantic will remain unchanged. If + modification of the permission policy is required for an existing contract, it + can be done by replacing a transfer hook only. No storage migration of the FA2 + ledger is required. + +* Transfer hooks could be used for purposes beyond permissioning, such as + implementing custom logic for a particular token application. + +### Transfer Hook Specification + +An FA2 token contract has a single entry point to set the hook. If a transfer hook +is not set, the FA2 token contract transfer operation MUST fail. Transfer hook is +to be set by the token contract administrator before any transfers can happen. +The concrete token contract implementation MAY impose additional restrictions on +who may set the hook. If the set hook operation is not permitted, it MUST fail +without changing existing hook configuration. + +For each transfer operation, a token contract MUST invoke a transfer hook and +return a corresponding operation as part of the transfer entry point result. +(For more details see [`set_transfer_hook`](#set_transfer_hook) ) + +`operator` parameter for the hook invocation MUST be set to `SENDER`. + +`from_` parameter for each `hook_transfer` batch entry MUST be set to `Some(transfer.from_)`. + +`to_` parameter for each `hook_transfer` batch entry MUST be set to `Some(transfer.to_)`. + +A transfer hook MUST be invoked, and operation returned by the hook invocation +MUST be returned by `transfer` entry point among other operations it might create. +`SENDER` MUST be passed as an `operator` parameter to any hook invocation. If an +invoked hook fails, the whole transfer transaction MUST fail. + +FA2 does NOT specify an interface for mint and burn operations; however, if an +FA2 token contract implements mint and burn operations, these operations MUST +invoke a transfer hook as well. + +| Mint | Burn | +| :---- | :--- | +| Invoked if registered. `from_` parameter MUST be `None` | Invoked if registered. `to_` parameter MUST be `None`| + +Note that using the transfer hook design pattern with sender/receiver hooks may +potentially be insecure. Sender and/or receiver contract hooks will be called +from the transfer hook contract (not the facade FA2 token contract). If sender/receiver +contracts rely on `SENDER` value for authorization, they must guarantee that the +call is initiated on behalf of the FA2 contract. + +### `set_transfer_hook` + +FA2 entry point with the following signature. + +LIGO definition: + +```ocaml +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type set_hook_param = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor; +} + +| Set_transfer_hook of set_hook_param_michelson +``` + +where + +```ocaml +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param_aux = { + fa2 : address; + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb + +type set_hook_param_aux = { + hook : unit -> transfer_descriptor_param_michelson contract; + permissions_descriptor : permissions_descriptor_michelson; +} + +type set_hook_param_michelson = set_hook_param_aux michelson_pair_right_comb +``` + +Michelson definition: + +``` +(pair %set_transfer_hook + (lambda %hook + unit + (contract + (pair + (address %fa2) + (pair + (list %batch + (pair + (option %from_ address) + (list %txs + (pair + (option %to_ address) + (pair + (nat %token_id) + (nat %amount) + ) + ) + ) + ) + ) + (address %operator) + ) + ) + ) + ) + (pair %permissions_descriptor + (or %operator + (unit %no_transfer) + (or + (unit %owner_transfer) + (unit %owner_or_operator_transfer) + ) + ) + (pair + (or %receiver + (unit %owner_no_op) + (or + (unit %optional_owner_hook) + (unit %required_owner_hook) + ) + ) + (pair + (or %sender + (unit %owner_no_op) + (or + (unit %optional_owner_hook) + (unit %required_owner_hook) + ) + ) + (option %custom + (pair + (string %tag) + (option %config_api address) + ) + ) + ) + ) + ) +) +``` + +FA2 implementation MAY restrict access to this operation to a contract administrator +address only. + +The parameter is an address plus hook entry point of type +`transfer_descriptor_param`. + +The transfer hook is always invoked from the `transfer` operation; otherwise, FA2 +MUST fail. + +`hook` field in `set_hook_param` record is a lambda which returns a hook entry +point of type `transfer_descriptor_param`. It allows a policy contract implementor +to choose a name for the hook entry point or even implement several transfer hooks +in the same contract. + +### Transfer Hook Examples + +#### Default Permission Policy + +Only a token owner can initiate a transfer of tokens from their accounts ( `from_` +MUST be equal to `SENDER`). + +Any address can be a recipient of the token transfer. + +[Hook contract](./examples/fa2_default_hook.mligo) + +#### Custom Receiver Hook/White List Permission Policy + +This is a sample implementation of the FA2 transfer hook, which supports receiver +whitelist and `fa2_token_receiver` for token receivers. The hook contract also +supports [operators](#operator-transfer-behavior). + +Only addresses that are whitelisted or implement the `fa2_token_receiver` interface +can receive tokens. If one or more `to_` addresses in FA2 transfer batch are not +permitted, the whole transfer operation MUST fail. + +The following table demonstrates the required actions depending on `to_` address +properties. + +| `to_` is whitelisted | `to_` implements `fa2_token_receiver` interface | Action | +| ------ | ----- | ----------| +| No | No | Transaction MUST fail | +| Yes | No | Continue transfer | +| No | Yes | Continue transfer, MUST call `tokens_received` | +| Yes | Yes | Continue transfer, MUST call `tokens_received` | + +Permission policy formula `S(true) * O(true) * ROH(None) * SOH(Custom)`. + +[Hook contract](./examples/fa2_custom_receiver.mligo) diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_convertors.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_convertors.mligo new file mode 100644 index 0000000..e31a506 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_convertors.mligo @@ -0,0 +1,168 @@ +(** +Helper function to convert FA2 entry points input parameters between their +Michelson and internal LIGO representation. + +FA2 contract implementation must conform to the Michelson entry points interface +outlined in the FA2 standard for interoperability with other contracts and off-chain +tools. + *) + +#if !FA2_CONVERTORS +#define FA2_CONVERTORS + +#include "../fa2_interface.mligo" + +let permissions_descriptor_to_michelson (d : permissions_descriptor) + : permissions_descriptor_michelson = + let aux : permissions_descriptor_aux = { + operator = Layout.convert_to_right_comb d.operator; + receiver = Layout.convert_to_right_comb d.receiver; + sender = Layout.convert_to_right_comb d.sender; + custom = match d.custom with + | None -> (None : custom_permission_policy_michelson option) + | Some c -> Some (Layout.convert_to_right_comb c) + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_to_michelson (p : transfer_descriptor) : transfer_descriptor_michelson = + let aux : transfer_descriptor_aux = { + from_ = p.from_; + txs = List.map + (fun (tx : transfer_destination_descriptor) -> + Layout.convert_to_right_comb tx + ) + p.txs; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_param_to_michelson (p : transfer_descriptor_param) + : transfer_descriptor_param_michelson = + let aux : transfer_descriptor_param_aux = { + operator = p.operator; + batch = List.map transfer_descriptor_to_michelson p.batch; + } in + Layout.convert_to_right_comb aux + +let transfer_descriptor_from_michelson (p : transfer_descriptor_michelson) : transfer_descriptor = + let aux : transfer_descriptor_aux = Layout.convert_from_right_comb p in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_descriptor_michelson) -> + let tx : transfer_destination_descriptor = + Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfer_descriptor_param_from_michelson (p : transfer_descriptor_param_michelson) + : transfer_descriptor_param = + let aux : transfer_descriptor_param_aux = Layout.convert_from_right_comb p in + let b : transfer_descriptor list = + List.map transfer_descriptor_from_michelson aux.batch + in + { + operator = aux.operator; + batch = b; + } + +let transfer_from_michelson (txm : transfer_michelson) : transfer = + let aux : transfer_aux = Layout.convert_from_right_comb txm in + { + from_ = aux.from_; + txs = List.map + (fun (txm : transfer_destination_michelson) -> + let tx : transfer_destination = Layout.convert_from_right_comb txm in + tx + ) + aux.txs; + } + +let transfers_from_michelson (txsm : transfer_michelson list) : transfer list = + List.map transfer_from_michelson txsm + +let transfer_to_michelson (tx : transfer) : transfer_michelson = + let aux : transfer_aux = { + from_ = tx.from_; + txs = List.map + (fun (tx: transfer_destination) -> + let t : transfer_destination_michelson = Layout.convert_to_right_comb tx in + t + ) tx.txs; + } in + Layout.convert_to_right_comb aux + +let transfers_to_michelson (txs : transfer list) : transfer_michelson list = + List.map transfer_to_michelson txs + +let operator_param_from_michelson (p : operator_param_michelson) : operator_param = + let op : operator_param = Layout.convert_from_right_comb p in + op + +let operator_param_to_michelson (p : operator_param) : operator_param_michelson = + Layout.convert_to_right_comb p + +let operator_update_from_michelson (uom : update_operator_michelson) : update_operator = + let aux : update_operator_aux = Layout.convert_from_right_comb uom in + match aux with + | Add_operator opm -> Add_operator_p (operator_param_from_michelson opm) + | Remove_operator opm -> Remove_operator_p (operator_param_from_michelson opm) + +let operator_update_to_michelson (uo : update_operator) : update_operator_michelson = + let aux = match uo with + | Add_operator_p op -> Add_operator (operator_param_to_michelson op) + | Remove_operator_p op -> Remove_operator (operator_param_to_michelson op) + in + Layout.convert_to_right_comb aux + +let operator_updates_from_michelson (updates_michelson : update_operator_michelson list) + : update_operator list = + List.map operator_update_from_michelson updates_michelson + +let balance_of_param_from_michelson (p : balance_of_param_michelson) : balance_of_param = + let aux : balance_of_param_aux = Layout.convert_from_right_comb p in + let requests = List.map + (fun (rm : balance_of_request_michelson) -> + let r : balance_of_request = Layout.convert_from_right_comb rm in + r + ) + aux.requests + in + { + requests = requests; + callback = aux.callback; + } + +let balance_of_param_to_michelson (p : balance_of_param) : balance_of_param_michelson = + let aux : balance_of_param_aux = { + requests = List.map + (fun (r : balance_of_request) -> Layout.convert_to_right_comb r) + p.requests; + callback = p.callback; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_to_michelson (r : balance_of_response) : balance_of_response_michelson = + let aux : balance_of_response_aux = { + request = Layout.convert_to_right_comb r.request; + balance = r.balance; + } in + Layout.convert_to_right_comb aux + +let balance_of_response_from_michelson (rm : balance_of_response_michelson) : balance_of_response = + let aux : balance_of_response_aux = Layout.convert_from_right_comb rm in + let request : balance_of_request = Layout.convert_from_right_comb aux.request in + { + request = request; + balance = aux.balance; + } + +let token_metas_to_michelson (ms : token_metadata list) : token_metadata_michelson list = + List.map + ( fun (m : token_metadata) -> + let mm : token_metadata_michelson = Layout.convert_to_right_comb m in + mm + ) ms + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_operator_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_operator_lib.mligo new file mode 100644 index 0000000..eb15ab0 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_operator_lib.mligo @@ -0,0 +1,88 @@ +(** +Reference implementation of the FA2 operator storage, config API and +helper functions +*) + +#if !FA2_OPERATOR_LIB +#define FA2_OPERATOR_LIB + +#include "fa2_convertors.mligo" +#include "../fa2_errors.mligo" + +(** +(owner, operator) -> unit +To be part of FA2 storage to manage permitted operators +*) +type operator_storage = ((address * address), unit) big_map + +(** + Updates operator storage using an `update_operator` command. + Helper function to implement `Update_operators` FA2 entry point +*) +let update_operators (update, storage : update_operator * operator_storage) + : operator_storage = + match update with + | Add_operator_p op -> + Big_map.update (op.owner, op.operator) (Some unit) storage + | Remove_operator_p op -> + Big_map.remove (op.owner, op.operator) storage + +(** +Validate if operator update is performed by the token owner. +@param updater an address that initiated the operation; usually `Tezos.sender`. +*) +let validate_update_operators_by_owner (update, updater : update_operator * address) + : unit = + let op = match update with + | Add_operator_p op -> op + | Remove_operator_p op -> op + in + if op.owner = updater then unit else failwith fa2_not_owner + +(** +Create an operator validator function based on provided operator policy. +@param tx_policy operator_transfer_policy defining the constrains on who can transfer. + *) +let make_operator_validator (tx_policy : operator_transfer_policy) + : (address * operator_storage)-> unit = + let can_owner_tx, can_operator_tx = match tx_policy with + | No_transfer -> (failwith fa2_tx_denied : bool * bool) + | Owner_transfer -> true, false + | Owner_or_operator_transfer -> true, true + in + let operator : address = Tezos.sender in + (fun (owner, ops_storage : address * operator_storage) -> + if can_owner_tx && owner = operator + then unit + else + if not can_operator_tx + then failwith fa2_not_owner + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith fa2_not_operator + ) + +(** +Default implementation of the operator validation function. +The default implicit `operator_transfer_policy` value is `Owner_or_operator_transfer` + *) +let make_default_operator_validator (operator : address) + : (address * operator_storage)-> unit = + (fun (owner, ops_storage : address * operator_storage) -> + if owner = operator + then unit + else + if Big_map.mem (owner, operator) ops_storage + then unit else failwith fa2_not_operator + ) + +(** +Validate operators for all transfers in the batch at once +@param tx_policy operator_transfer_policy defining the constrains on who can transfer. +*) +let validate_operator (tx_policy, txs, ops_storage + : operator_transfer_policy * (transfer list) * operator_storage) : unit = + let validator = make_operator_validator tx_policy in + List.iter (fun (tx : transfer) -> validator (tx.from_, ops_storage)) txs + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_owner_hooks_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_owner_hooks_lib.mligo new file mode 100644 index 0000000..35a9af1 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_owner_hooks_lib.mligo @@ -0,0 +1,149 @@ +#if !FA2_BEHAVIORS +#define FA2_BEHAVIORS + +(** +Generic implementation of the permission logic for sender and receiver hooks. +Actual behavior is driven by a `permissions_descriptor`. +To be used in FA2 and/or FA2 permission transfer hook contract implementation +which supports sender/receiver hooks. +*) + +#include "../fa2_interface.mligo" +#include "../fa2_errors.mligo" + +type get_owners = transfer_descriptor -> (address option) list + +type hook_entry_point = transfer_descriptor_param_michelson contract + +type hook_result = + | Hook_entry_point of hook_entry_point + | Hook_undefined of string + +type to_hook = address -> hook_result + +(* type transfer_hook_params = { + ligo_param : transfer_descriptor_param; + michelson_param : transfer_descriptor_param_michelson; +} *) + +(** +Extracts a set of unique `from_` or `to_` addresses from the transfer batch. +@param batch transfer batch +@param get_owner selector of `from_` or `to_` addresses from each individual `transfer_descriptor` + *) +let get_owners_from_batch (batch, get_owners : (transfer_descriptor list) * get_owners) : address set = + List.fold + (fun (acc, tx : (address set) * transfer_descriptor) -> + let owners = get_owners tx in + List.fold + (fun (acc, o: (address set) * (address option)) -> + match o with + | None -> acc + | Some a -> Set.add a acc + ) + owners + acc + ) + batch + (Set.empty : address set) + +let validate_owner_hook (p, get_owners, to_hook, is_required : + transfer_descriptor_param * get_owners * to_hook * bool) + : hook_entry_point list = + let owners = get_owners_from_batch (p.batch, get_owners) in + Set.fold + (fun (eps, owner : (hook_entry_point list) * address) -> + match to_hook owner with + | Hook_entry_point h -> h :: eps + | Hook_undefined error -> + (* owner hook is not implemented by the target contract *) + if is_required + then (failwith error : hook_entry_point list) (* owner hook is required: fail *) + else eps (* owner hook is optional: skip it *) + ) + owners ([] : hook_entry_point list) + +let validate_owner(p, policy, get_owners, to_hook : + transfer_descriptor_param * owner_hook_policy * get_owners * to_hook) + : hook_entry_point list = + match policy with + | Owner_no_hook -> ([] : hook_entry_point list) + | Optional_owner_hook -> validate_owner_hook (p, get_owners, to_hook, false) + | Required_owner_hook -> validate_owner_hook (p, get_owners, to_hook, true) + +(** +Given an address of the token receiver, tries to get an entry point for +`fa2_token_receiver` interface. + *) +let to_receiver_hook : to_hook = fun (a : address) -> + let c : hook_entry_point option = + Operation.get_entrypoint_opt "%tokens_received" a in + match c with + | Some c -> Hook_entry_point c + | None -> Hook_undefined fa2_receiver_hook_undefined + +(** +Create a list iof Tezos operations invoking all token receiver contracts that +implement `fa2_token_receiver` interface. Fail if specified `owner_hook_policy` +cannot be met. + *) +let validate_receivers (p, receiver_policy : transfer_descriptor_param * owner_hook_policy) + : hook_entry_point list = + let get_receivers : get_owners = fun (tx : transfer_descriptor) -> + List.map (fun (t : transfer_destination_descriptor) -> t.to_ )tx.txs in + validate_owner (p, receiver_policy, get_receivers, to_receiver_hook) + +(** +Given an address of the token sender, tries to get an entry point for +`fa2_token_sender` interface. + *) +let to_sender_hook : to_hook = fun (a : address) -> + let c : hook_entry_point option = + Operation.get_entrypoint_opt "%tokens_sent" a in + match c with + | Some c -> Hook_entry_point c + | None -> Hook_undefined fa2_sender_hook_undefined + +(** +Create a list iof Tezos operations invoking all token sender contracts that +implement `fa2_token_sender` interface. Fail if specified `owner_hook_policy` +cannot be met. + *) +let validate_senders (p, sender_policy : transfer_descriptor_param * owner_hook_policy) + : hook_entry_point list = + let get_sender : get_owners = fun (tx : transfer_descriptor) -> [tx.from_] in + validate_owner (p, sender_policy, get_sender, to_sender_hook) + +(** +Generate a list of Tezos operations invoking sender and receiver hooks according to +the policies defined by the permissions descriptor. +To be used in FA2 and/or FA2 transfer hook contract implementation which supports +sender/receiver hooks. + *) +let owners_transfer_hook (p, descriptor : transfer_descriptor_param * permissions_descriptor) + : hook_entry_point list = + let sender_entries = validate_senders (p, descriptor.sender) in + let receiver_entries = validate_receivers (p, descriptor.receiver) in + (* merge two lists *) + List.fold + (fun (l, ep : (hook_entry_point list) * hook_entry_point) -> ep :: l) + receiver_entries sender_entries + +let transfers_to_descriptors (txs : transfer list) : transfer_descriptor list = + List.map + (fun (tx : transfer) -> + let txs = List.map + (fun (dst : transfer_destination) -> + { + to_ = Some dst.to_; + token_id = dst.token_id; + amount = dst.amount; + } + ) tx.txs in + { + from_ = Some tx.from_; + txs = txs; + } + ) txs + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_transfer_hook_lib.mligo b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_transfer_hook_lib.mligo new file mode 100644 index 0000000..a4c99c1 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/lib/fa2_transfer_hook_lib.mligo @@ -0,0 +1,49 @@ +(** + Helper types and functions to implement transfer hook contract. + Each transfer hook contract maintains a registry of known FA2 contracts and + validates that it is invoked from registered FA2 contracts. + + The implementation assumes that the transfer hook entry point is labeled as + `%tokens_transferred_hook`. + *) + +#if !FA2_HOOK_LIB +#define FA2_HOOK_LIB + +#include "../fa2_hook.mligo" +#include "fa2_convertors.mligo" + +let get_hook_entrypoint (hook_contract : address) (u : unit) + : transfer_descriptor_param_michelson contract = + let hook_entry : transfer_descriptor_param_michelson contract = + Operation.get_entrypoint "%tokens_transferred_hook" hook_contract in + hook_entry + + +let create_register_hook_op + (fa2, descriptor : (fa2_with_hook_entry_points contract) * permissions_descriptor) : operation = + let hook_fn = get_hook_entrypoint Current.self_address in + let p : set_hook_param_aux = { + hook = hook_fn; + permissions_descriptor = permissions_descriptor_to_michelson descriptor; + } in + let pm = Layout.convert_to_right_comb p in + Operation.transaction (Set_transfer_hook pm) 0mutez fa2 + + +type fa2_registry = address set + +let register_with_fa2 (fa2, descriptor, registry : + (fa2_with_hook_entry_points contract) * permissions_descriptor * fa2_registry) + : operation * fa2_registry = + let op = create_register_hook_op (fa2, descriptor) in + let fa2_address = Current.address fa2 in + let new_registry = Set.add fa2_address registry in + op, new_registry + +let validate_hook_call (fa2, registry: address * fa2_registry) : unit = + if Set.mem fa2 registry + then unit + else failwith "UNKNOWN_FA2_CALL" + +#endif diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/tzip-12.md b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/tzip-12.md new file mode 100644 index 0000000..cf1f849 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Operator/tzip-12/tzip-12.md @@ -0,0 +1,1093 @@ +--- +tzip: 12 +title: FA2 - Multi-Asset Interface +status: Draft +type: Financial Application (FA) +author: Eugene Mishura (@e-mishura) +created: 2020-01-24 +--- + +## Table Of Contents + +* [Summary](#summary) +* [Motivation](#motivation) +* [Abstract](#abstract) +* [Interface Specification](#interface-specification) + * [Entry Point Semantics](#entry-point-semantics) + * [`transfer`](#transfer) + * [Core `transfer` Behavior](#core-transfer-behavior) + * [Default `transfer` Permission Policy](#default-transfer-permission-policy) + * [`balance_of`](#balance_of) + * [Operators](#operators) + * [`update_operators`](#update_operators) + * [Token Metadata](#token-metadata) + * [`token_metadata_registry`](#token_metadata_registry) + * [`token_metadata` `big_map`](#token_metadata-big_map) + * [`token_metadata` Entry Point](#token_metadata-entry-point) + * [FA2 Permission Policies and Configuration](#fa2-permission-policies-and-configuration) + * [A Taxonomy of Permission Policies](#a-taxonomy-of-permission-policies) + * [`permissions_descriptor`](#permissions_descriptor) + * [Error Handling](#error-handling) +* [Implementing Different Token Types with FA2](#implementing-different-token-types-with-fa2) + * [Single Fungible Token](#single-fungible-token) + * [Multiple Fungible Tokens](#multiple-fungible-tokens) + * [Non-fungible Tokens](#non-fungible-tokens) + * [Mixing Fungible and Non-fungible Tokens](#mixing-fungible-and-non-fungible-tokens) + * [Non-transferable Tokens](#non-transferable-tokens) +* [Future Directions](#future-directions) +* [Copyright](#copyright) + +## Summary + +TZIP-12 proposes a standard for a unified token contract interface, +supporting a wide range of token types and implementations. This document provides +an overview and rationale for the interface, token transfer semantics, and support +for various transfer permission policies. + +**PLEASE NOTE:** This API specification remains a work-in-progress and may evolve +based on public comment see FA2 Request for Comment on [Tezos Agora](https://tezosagora.org). + +## Motivation + +There are multiple dimensions and considerations while implementing a particular +token smart contract. Tokens might be fungible or non-fungible. A variety of +permission policies can be used to define how many tokens can be transferred, who +can perform a transfer, and who can receive tokens. A token contract can be +designed to support a single token type (e.g. ERC-20 or ERC-721) or multiple token +types (e.g. ERC-1155) to optimize batch transfers and atomic swaps of the tokens. + +Such considerations can easily lead to the proliferation of many token standards, +each optimized for a particular token type or use case. This situation is apparent +in the Ethereum ecosystem, where many standards have been proposed, but ERC-20 +(fungible tokens) and ERC-721 (non-fungible tokens) are dominant. + +Token wallets, token exchanges, and other clients then need to support multiple +standards and multiple token APIs. The FA2 standard proposes a unified token +contract interface that accommodates all mentioned concerns. It aims to provide +significant expressivity to contract developers to create new types of tokens +while maintaining a common interface standard for wallet integrators and external +developers. + +## Abstract + +Token type is uniquely identified on the chain by a pair composed of the token +contract address and token ID, a natural number (`nat`). If the underlying contract +implementation supports only a single token type (e.g. ERC-20-like contract), +the token ID MUST be `0n`. In the case, when multiple token types are supported +within the same FA2 token contract (e. g. ERC-1155-like contract), the contract +is fully responsible for assigning and managing token IDs. FA2 clients MUST NOT +depend on particular ID values to infer information about a token. + +Most of the entry points are batch operations that allow querying or transfer of +multiple token types atomically. If the underlying contract implementation supports +only a single token type, the batch may contain single or multiple entries where +token ID will be a fixed `0n` value. Likewise, if multiple token types are supported, +the batch may contain zero or more entries and there may be duplicate token IDs. + +Most token standards specify logic that validates a transfer transaction and can +either approve or reject a transfer. Such logic could validate who can perform a +transfer, the transfer amount, and who can receive tokens. This standard calls such +logic a *permission policy*. The FA2 standard defines the +[default `transfer` permission policy](#default-transfer-permission-policy) that +specify who can transfer tokens. The default policy allows transfers by +either token owner (an account that holds token balance) or by an operator +(an account that is permitted to manage tokens on behalf of the token owner). + +Unlike many other standards, FA2 allows to customize the default permission policy +(see [FA2 Permission Policies and Configuration](#fa2-permission-policies-and-configuration)) +using a set of predefined permission policies that are optional. + +This specification defines the set of [standard errors](#error-handling) and error +mnemonics to be used when implementing FA2. However, some implementations MAY +introduce their custom errors that MUST follow the same pattern as standard ones. + +## Interface Specification + +Token contract implementing the FA2 standard MUST have the following entry points. +Notation is given in [cameLIGO language](https://ligolang.org) for readability +and Michelson. The LIGO definition, when compiled, generates compatible Michelson +entry points. + +`type fa2_entry_points =` + +* [`| Transfer of transfer list`](#transfer) +* [`| Balance_of of balance_of_param`](#balance_of) +* [`| Update_operators of update_operator list`](#update_operators) +* [`| Token_metadata_registry of address contract`](##token_metadata_registry) + +The full definition of the FA2 entry points in LIGO and related types can be found +in [fa2_interface.mligo](./fa2_interface.mligo). + +### Entry Point Semantics + +#### `transfer` + +LIGO definition: + +```ocaml +type token_id = nat + +type transfer_destination = { + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer = { + from_ : address; + txs : transfer_destination list; +} + +| Transfer of transfer_michelson list +``` + +
+where + +```ocaml +type transfer_destination_michelson = transfer_destination michelson_pair_right_comb + +type transfer_aux = { + from_ : address; + txs : transfer_destination_michelson list; +} + +type transfer_michelson = transfer_aux michelson_pair_right_comb +``` + +
+ +Michelson definition: +``` +(list %transfer + (pair + (address %from_) + (list %txs + (pair + (address %to_) + (pair + (nat %token_id) + (nat %amount) + ) + ) + ) + ) +) +``` + +Each transfer in the batch is specified between one source (`from_`) address and +a list of destinations. Each `transfer_destination` specifies token type and the +amount to be transferred from the source address to the destination (`to_`) address. + +FA2 does NOT specify an interface for mint and burn operations; however, if an +FA2 token contract implements mint and burn operations, it SHOULD, when possible, +enforce the same rules and logic applied to the token transfer operation. Mint +and burn can be considered special cases of the transfer. Although, it is possible +that mint and burn have more or less restrictive rules than the regular transfer. +For instance, mint and burn operations may be invoked by a special privileged +administrative address only. In this case, regular operator restrictions may not +be applicable. + +##### Core `transfer` Behavior + +FA2 token contracts MUST always implement this behavior. + +* Every transfer operation MUST happen atomically and in order. If at least one + transfer in the batch cannot be completed, the whole transaction MUST fail, all + token transfers MUST be reverted, and token balances MUST remain unchanged. + +* Each individual transfer MUST decrement token balance of the source (`from_`) + address by the amount of the transfer and increment token balance of the destination + (`to_`) address by the amount of the transfer. + +* If the transfer amount exceeds current token balance of the source address, + the whole transfer operation MUST fail with the error mnemonic `"FA2_INSUFFICIENT_BALANCE"`. + +* If the token owner does not hold any tokens of type `token_id`, the owner's balance + is interpreted as zero. No token owner can have a negative balance. + +* The transfer MUST update token balances exactly as the operation + parameters specify it. Transfer operations MUST NOT try to adjust transfer + amounts or try to add/remove additional transfers like transaction fees. + +* Transfers of zero amount MUST be treated as normal transfers. + +* If one of the specified `token_id`s is not defined within the FA2 contract, the + entry point MUST fail with the error mnemonic `"FA2_TOKEN_UNDEFINED"`. + +* Transfer implementations MUST apply permission policy logic (either + [default transfer permission policy](#default-transfer-permission-policy) or + [customized one](#customizing-permission-policy)). + If permission logic rejects a transfer, the whole operation MUST fail. + +* Core transfer behavior MAY be extended. If additional constraints on tokens + transfer are required, FA2 token contract implementation MAY invoke additional + permission policies. If the additional permission fails, the whole transfer + operation MUST fail with a custom error mnemonic. + +##### Default `transfer` Permission Policy + +* Token owner address MUST be able to perform a transfer of its own tokens (e. g. + `SENDER` equals to `from_` parameter in the `transfer`). + +* An operator (a Tezos address that performs token transfer operation on behalf + of the owner) MUST be permitted to manage all owner's tokens before it invokes + a transfer transaction (see [`update_operators`](#update_operators)). + +* If the address that invokes a transfer operation is neither a token owner nor + one of the permitted operators, the transaction MUST fail with the error mnemonic + `"FA2_NOT_OPERATOR"`. If at least one of the `transfer`s in the batch is not permitted, + the whole transaction MUST fail. + +#### `balance_of` + +LIGO definition: + +```ocaml +type token_id = nat + +type balance_of_request = { + owner : address; + token_id : token_id; +} + +type balance_of_response = { + request : balance_of_request; + balance : nat; +} + +type balance_of_param = { + requests : balance_of_request list; + callback : (balance_of_response_michelson list) contract; +} + +| Balance_of of balance_of_param_michelson +``` + +
+where + +```ocaml +type balance_of_request_michelson = balance_of_request michelson_pair_right_comb + +type balance_of_response_aux = { + request : balance_of_request_michelson; + balance : nat; +} + +type balance_of_response_michelson = balance_of_response_aux michelson_pair_right_comb + +type balance_of_param_aux = { + requests : balance_of_request_michelson list; + callback : (balance_of_response_michelson list) contract; +} + +type balance_of_param_michelson = balance_of_param_aux michelson_pair_right_comb +``` +
+ +Michelson definition: + +``` +(pair %balance_of + (list %requests + (pair + (address %owner) + (nat %token_id) + ) + ) + (contract %callback + (list + (pair + (pair %request + (address %owner) + (nat %token_id) + ) + (nat %balance) + ) + ) + ) +) +``` + +Get the balance of multiple account/token pairs. Accepts a list of +`balance_of_request`s and a callback contract `callback` which accepts a list of +`balance_of_response` records. + +* There may be duplicate `balance_of_request`'s, in which case they should not be + deduplicated nor reordered. + +* If the account does not hold any tokens, the account + balance is interpreted as zero. + +* If one of the specified `token_id`s is not defined within the FA2 contract, the + entry point MUST fail with the error mnemonic `"FA2_TOKEN_UNDEFINED"`. + +*Notice:* The `balance_of` entry point implements a *continuation-passing style (CPS) +view entry point* pattern that invokes the other callback contract with the requested +data. This pattern, when not used carefully, could expose the callback contract +to an inconsistent state and/or manipulatable outcome (see +[view patterns](https://www.notion.so/Review-of-TZIP-12-95e4b631555d49429e2efdfe0f9ffdc0#6d68e18802734f059adf3f5ba8f32a74)). +The `balance_of` entry point should be used on the chain with the extreme caution. + +#### Operators + +**Operator** is a Tezos address that originates token transfer operation on behalf +of the owner. + +**Owner** is a Tezos address which can hold tokens. + +An operator, other than the owner, MUST be approved to manage all tokens held by +the owner to make a transfer from the owner account. + +FA2 interface specifies two entry points to update and inspect operators. Once +permitted for the specific token owner, an operator can transfer any tokens belonging +to the owner. + +##### `update_operators` + +LIGO definition: + +```ocaml +type operator_param = { + owner : address; + operator : address; +} + +type update_operator = + | Add_operator_p of operator_param + | Remove_operator_p of operator_param + +| Update_operators of update_operator_michelson list +``` + +
+where + +```ocaml +type operator_param_michelson = operator_param michelson_pair_right_comb + +type update_operator_aux = + | Add_operator of operator_param_michelson + | Remove_operator of operator_param_michelson + +type update_operator_michelson = update_operator_aux michelson_or_right_comb +``` + +
+ +Michelson definition: + +``` +(list %update_operators + (or + (pair %add_operator + (address %owner) + (address %operator) + ) + (pair %remove_operator + (address %owner) + (address %operator) + ) + ) +) +``` + +Add or Remove token operators for the specified owners. + +* The entry point accepts a list of `update_operator` commands. If two different + commands in the list add and remove an operator for the same owner, + the last command in the list MUST take effect. + +* It is possible to update operators for a token owner that does not hold any token + balances yet. + +* Operator relation is not transitive. If C is an operator of B , and if B is an + operator of A, C cannot transfer tokens that are owned by A, on behalf of B. + +The standard does not specify who is permitted to update operators on behalf of +the token owner. Depending on the business use case, the particular implementation +of the FA2 contract MAY limit operator updates to a token owner (`owner == SENDER`) +or be limited to an administrator. + +#### Token Metadata + +Each FA2 `token_id` has associated metadata of the following type: + +```ocaml +type token_id = nat + +type token_metadata = { + token_id : token_id; + symbol : string; + name : string; + decimals : nat; + extras : (string, string) map; +} +``` + +Michelson definition: + +``` +(pair + (nat %token_id) + (pair + (string %symbol) + (pair + (string %name) + (pair + (nat %decimals) + (map %extras string string) +)))) +``` + +* FA2 token amounts are represented by natural numbers (`nat`), and their + **granularity** (the smallest amount of tokens which may be minted, burned, or + transferred) is always 1. + +* `decimals` is the number of digits to use after the decimal point when displaying + the token amounts. If 0, the asset is not divisible. Decimals are used for display + purposes only and MUST NOT affect transfer operation. + +Examples + +| Decimals | Amount | Display | +| -------- | ------- | -------- | +| 0n | 123 | 123 | +| 1n | 123 | 12.3 | +| 3n | 123000 | 123 | + +Token metadata is primarily useful in off-chain, user-facing contexts (e.g. +wallets, explorers, marketplaces). As a result, FA2 optimizes for off-chain use +of token metadata and minimal on-chain gas consumption. A related effort to create +a separate metadata TZIP standard is also underway. + +* The FA2 contract MUST implement `token_metadata_registry` view entry point that + returns an address of the contract holding tokens metadata. Token metadata can + be held either by the FA2 token contract itself (then `token_metadata_registry` + returns `SELF` address) or by a separate token registry contract. + +* Token registry contract MUST implement one of two ways to expose token metadata + for off-chain clients: + + * Contract storage MUST have a `big_map` that maps `token_id -> token_metadata` + and annotated `%token_metadata` + + * Contract MUST implement entry point `token_metadata` + +##### `token_metadata_registry` + +LIGO definition: + +```ocaml +| Token_metadata_registry of address contract +``` + +Michelson definition: + +``` +(contract %token_metadata_registry address) +``` + +Return address of the contract that holds tokens metadata. If the FA2 contract +holds its own tokens metadata, the entry point returns `SELF` address. The entry +point parameter is some contract entry point to be called with the address of the +token metadata registry. + +##### `token_metadata` `big_map` + +LIGO definition: + +```ocaml +type = { + ... + token_metadata : (token_id, token_metadata) big_map; + ... +} +``` + +Michelson definition: + +``` +(big_map %token_metadata + nat + (pair + (nat %token_id) + (pair + (string %symbol) + (pair + (string %name) + (pair + (nat %decimals) + (map %extras string string) + )))) +) +``` + +The FA2 contract storage MUST have a `big_map` with a key type `token_id` and +value type `token_metadata`. This `big_map` MUST be annotated as `%token_metadata` +and can be at any position within the storage. + +##### `token_metadata` Entry Point + +LIGO definition: + +```ocaml +type token_metadata_param = { + token_ids : token_id list; + handler : (token_metadata_michelson list) -> unit; +} + +| Token_metadata of token_metadata_param_michelson +``` + +
+where + +```ocaml +type token_metadata_michelson = token_metadata michelson_pair_right_comb + +type token_metadata_param_michelson = token_metadata_param michelson_pair_right_comb +``` + +Michelson definition: + +``` +(pair %token_metadata + (list %token_ids nat) + (lambda %handler + (list + (pair + (nat %token_id) + (pair + (string %symbol) + (pair + (string %name) + (pair + (nat %decimals) + (map %extras string string) + )))) + ) + unit + ) +) +``` + +
+ +Get the metadata for multiple token types. Accepts a list of `token_id`s and a +a lambda `handler`, which accepts a list of `token_metadata` records. The `handler` +lambda may assert certain assumptions about the metadata and/or fail with the +obtained metadata implementing a view entry point pattern to extract tokens metadata +off-chain. + +* As with `balance_of`, the input `token_id`'s should not be deduplicated nor + reordered. + +* If one of the specified `token_id`s is not defined within the FA2 contract, the + entry point MUST fail with the error mnemonic `"FA2_TOKEN_UNDEFINED"`. + +### FA2 Permission Policies and Configuration + +Most token standards specify logic such as who can perform a transfer, the amount +of a transfer, and who can receive tokens. This standard calls such logic *permission +policy* and defines a framework to compose such permission policies from the +[standard behaviors](#permission-behaviors). + +The FA2 contract developer can choose and implement a custom set of permissions +behaviors. The particular implementation may be static (the permissions configuration +cannot be changed after the contract is deployed) or dynamic (the FA2 contract +may be upgradable and allow to change the permissions configuration). At any moment +in time, the FA2 token contract MUST expose consistent and non-self-contradictory +permissions configuration (unlike ERC-777 that exposes two flavors of the transfer +at the same time). + +#### A Taxonomy of Permission Policies + +##### Permission Behaviors + +Permission policy semantics are composed from several orthogonal behaviors. +The concrete policy is expressed as a combination of those behaviors. Each permission +policy defines a set of possible standard behaviors. An FA2 contract developer MAY +chose to implement one or more behaviors that are different from the default ones +depending on their business use case. + +The FA2 defines the following standard permission behaviors, that can be chosen +independently, when an FA2 contract is implemented: + +###### `Operator` Permission Behavior + +This behavior specifies who is permitted to transfer tokens. + +Potentially token transfers can be performed by the token owner or by an operator +permitted to transfer tokens on behalf of the token owner. An operator can transfer +any tokens in any amount on behalf of the owner. + +```ocaml +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer (* default *) +``` + +* `No_transfer` - neither owner nor operator can transfer tokens. This permission + configuration can be used for non-transferable tokens or for the FA2 implementation + when a transfer can be performed only by some privileged and/or administrative + account. The transfer operation MUST fail with the error mnemonic `"FA2_TX_DENIED"`. + +* `Owner_transfer` - If `SENDER` is not the token owner, the transfer operation + MUST fail with the error mnemonic `"FA2_NOT_OWNER"`. + +* `Owner_or_operator_transfer` - allows transfer for the token owner or an operator + permitted to manage tokens on behalf of the owner. If `SENDER` is not the token + owner and not an operator permitted to manage tokens on behalf of the owner, + the transfer operation MUST fail with the error mnemonic `"FA2_NOT_OPERATOR"`. + The FA2 standard defines the entry point to manage operators associated with + the token owner address ([`update_operators`](#update_operators)). Once an + operator is added, it can manage all of its associated owner's tokens. + +* If an operator transfer is denied (`No_transfer` or `Owner_transfer`), +[`update_operators`](#update_operators) entry point MUST fail if invoked with the +error mnemonic `"FA2_OPERATORS_UNSUPPORTED"`. + +###### `Token Owner Hook` Permission Behavior + +Each transfer operation accepts a batch that defines token owners that send tokens +(senders) and token owners that receive tokens (receivers). Token owner contracts +MAY implement either `fa2_token_sender` and/or `fa2_token_receiver` interfaces. +Those interfaces define a hook entry point that accepts transfer description and +invoked by the FA2 contract in the context of transfer, mint and burn operations. + +Token owner permission can be configured to behave in one of the following ways: + +```ocaml +type owner_hook_policy = + | Owner_no_hook (* default *) + | Optional_owner_hook + | Required_owner_hook +``` + +* `Owner_no_hook` - ignore the owner hook interface. + +* `Optional_owner_hook` - treat the owner hook interface as optional. If a token +owner contract implements a corresponding hook interface, it MUST be invoked. If +the hook interface is not implemented, it gets ignored. + +* `Required_owner_hook` - treat the owner hook interface as required. If a token +owner contract implements a corresponding hook interface, it MUST be invoked. If +the hook interface is not implemented, the entire transfer transaction MUST fail. + +* Sender and/or receiver hooks can approve the transaction or reject it + by failing. If such a hook is invoked and failed, the whole transfer operation + MUST fail. + +* This policy can be applied to both token senders and token receivers. There are + two owner hook interfaces, `fa2_token_receiver` and `fa2_token_sender`, that need + to be implemented by token owner contracts to expose the owner's hooks to FA2 token + contract. + +* If a transfer failed because of the token owner permission behavior, the operation + MUST fail with the one of the following error mnemonics: + +| Error Mnemonic | Description | +| :------------- | :---------- | +| `"FA2_RECEIVER_HOOK_FAILED"` | Receiver hook is invoked and failed. This error MUST be raised by the hook implementation | +| `"FA2_SENDER_HOOK_FAILED"` | Sender hook is invoked and failed. This error MUST be raised by the hook implementation | +| `"FA2_RECEIVER_HOOK_UNDEFINED"` | Receiver hook is required by the permission behavior, but is not implemented by a receiver contract | +| `"FA2_SENDER_HOOK_UNDEFINED"` | Sender hook is required by the permission behavior, but is not implemented by a sender contract | + +* `transfer_descriptor` type defined below can represent regular transfer, mint and + burn operations. + +| operation | `from_` | `to_` | +| :-------- | :------ | :----- | +| transfer | MUST be `Some sender_address` | MUST be `Some receiver_address` | +| mint | MUST be `None` | MUST be `Some receiver_address` | +| burn | MUST be `Some burner_address` | MUST be `None` | + +* If all of the following conditions are met, the FA2 contract MUST invoke both + `fa2_token_sender` and `fa2_token_receiver` entry points: + * the token owner implements both `fa2_token_sender` and `fa2_token_receiver` + interfaces + * the token owner receives and sends some tokens in the same transfer operation + * both sender and receiver hooks are enabled by the FA2 permissions policy + +* If the token owner participates in multiple transfers within the transfer operation + batch and hook invocation is required by the permissions policy, the hook MUST + be invoked only once. + +* The hooks MUST NOT be invoked in the context of the operation other than transfer, + mint and burn. + +* `transfer_descriptor_param.operator` MUST be initialized with the address that + invoked the FA2 contract (`SENDER`). + +A special consideration is required if FA2 implementation supports sender and/or +receiver hooks. It is possible that one of the token owner hooks will fail because +of the hook implementation defects or other circumstances out of control of the +FA2 contract. This situation may cause tokens to be permanently locked on the token +owner's account. One of the possible solutions could be the implementation of a +special administrative version of the mint and burn operations that bypasses owner's +hooks otherwise required by the FA2 contract permissions policy. + +```ocaml +type transfer_destination_descriptor = { + to_ : address option; + token_id : token_id; + amount : nat; +} + +type transfer_descriptor = { + from_ : address option; + txs : transfer_destination_descriptor list +} + +type transfer_descriptor_param = { + batch : transfer_descriptor list; + operator : address; +} + +type fa2_token_receiver = + | Tokens_received of transfer_descriptor_param_michelson + +type fa2_token_sender = + | Tokens_sent of transfer_descriptor_param_michelson +``` + +
+where + +```ocaml +type transfer_destination_descriptor_michelson = + transfer_destination_descriptor michelson_pair_right_comb + +type transfer_descriptor_aux = { + from_ : address option; + txs : transfer_destination_descriptor_michelson list +} + +type transfer_descriptor_michelson = transfer_descriptor_aux michelson_pair_right_comb + +type transfer_descriptor_param_aux = { + batch : transfer_descriptor_michelson list; + operator : address; +} + +type transfer_descriptor_param_michelson = transfer_descriptor_param_aux michelson_pair_right_comb +``` + +
+ +Michelson definition: + +``` +(pair + (list %batch + (pair + (option %from_ address) + (list %txs + (pair + (option %to_ address) + (pair + (nat %token_id) + (nat %amount) + ) + ) + ) + ) + ) + (address %operator) +) +``` + +##### Permission Policy Formulae + +Each concrete implementation of the permission policy can be described by a formula +which combines permission behaviors in the following form: + +``` +Operator(?) * Receiver(?) * Sender(?) +``` + +For instance, `Operator(Owner_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook)` +formula describes the policy which allows only token owners to transfer their own +tokens. + +`Operator(No_transfer) * Receiver(Owner_no_hook) * Sender(Owner_no_hook)` formula +represents non-transferable token (neither token owner, nor operators can transfer +tokens. + +Permission token policy formula is expressed by the `permissions_descriptor` type. + +```ocaml +type operator_transfer_policy = + | No_transfer + | Owner_transfer + | Owner_or_operator_transfer + +type owner_hook_policy = + | Owner_no_hook + | Optional_owner_hook + | Required_owner_hook + +type custom_permission_policy = { + tag : string; + config_api: address option; +} + +type permissions_descriptor = { + operator : operator_transfer_policy; + receiver : owner_hook_policy; + sender : owner_hook_policy; + custom : custom_permission_policy option; +} +``` + +It is possible to extend permission policy with a `custom` behavior, which does +not overlap with already existing standard policies. This standard does not specify +exact types for custom config entry points. FA2 token contract clients that support +custom config entry points must know their types a priori and/or use a `tag` hint +of `custom_permission_policy`. + +##### Customizing Permission Policy + +The FA2 contract MUST always implement the [core transfer behavior](#core-transfer-behavior). +However, FA2 contract developer MAY chose to implement either the +[default transfer permission policy](#default-transfer-permission-policy) or a +custom policy. +The FA2 contract implementation MAY customize one or more of the standard permission +behaviors (`operator`, `receiver`, `sender` as specified in `permissions_descriptor` +type), by choosing one of the available options for those permission behaviors. + +##### `permissions_descriptor` Entry Point + +LIGO definition: + +```ocaml +| Permissions_descriptor of permissions_descriptor_michelson contract +``` + +
+where + +```ocaml +type operator_transfer_policy_michelson = operator_transfer_policy michelson_or_right_comb + +type owner_hook_policy_michelson = owner_hook_policy michelson_or_right_comb + +type custom_permission_policy_michelson = custom_permission_policy michelson_pair_right_comb + +type permissions_descriptor_aux = { + operator : operator_transfer_policy_michelson; + receiver : owner_hook_policy_michelson; + sender : owner_hook_policy_michelson; + custom : custom_permission_policy_michelson option; +} + +type permissions_descriptor_michelson = permissions_descriptor_aux michelson_pair_right_comb +``` + +
+ +Michelson definition: + +``` +(contract %permissions_descriptor + (pair + (or %operator + (unit %no_transfer) + (or + (unit %owner_transfer) + (unit %owner_or_operator_transfer) + ) + ) + (pair + (or %receiver + (unit %owner_no_hook) + (or + (unit %optional_owner_hook) + (unit %required_owner_hook) + ) + ) + (pair + (or %sender + (unit %owner_no_hook) + (or + (unit %optional_owner_hook) + (unit %required_owner_hook) + ) + ) + (option %custom + (pair + (string %tag) + (option %config_api address) + ) + ) + ) + ) + ) +) +``` + +Get the descriptor of the transfer permission policy. FA2 specifies +`permissions_descriptor` allowing external contracts (e.g. an auction) to discover +an FA2 contract's implemented permission policies. + +The implicit value of the descriptor for the +[default `transfer` permission policy](#default-transfer-permission-policy) is +the following: + +```ocaml +let default_descriptor : permissions_descriptor = { + operator = Owner_or_operator_transfer; + receiver = Owner_no_hook; + sender = Owner_no_hook; + custom = (None: custom_permission_policy option); +} +``` + +* If the FA2 contract implements one or more non-default behaviors, it MUST implement + `permission_descriptor` entry point. The descriptor field values MUST reflect + actual permission behavior implemented by the contract. + +* If the FA2 contract implements the default permission policy, it MAY omit the + implementation of the `permissions_descriptor` entry point. + +* In addition to the standard permission behaviors, the FA2 contract MAY also + implement an optional custom permissions policy. If such custom a policy is + implemented, the FA2 contract SHOULD expose it using permissions descriptor + `custom` field. `custom_permission_policy.tag` value would be available to + other parties which are aware of such custom extension. Some custom permission + MAY require a config API (like [`update_operators`](#update_operators) entry + point of the FA2 to configure `operator_transfer_policy`). The address of the + contract that provides config entry points is specified by + `custom_permission_policy.config_api` field. The config entry points MAY be + implemented either within the FA2 token contract itself (then the returned + address SHALL be `SELF`), or in a separate contract. + +### Error Handling + +This specification defines the set of standard errors to make it easier to integrate +FA2 contracts with wallets, DApps and other generic software, and enable +localization of user-visible error messages. + +Each error code is a short abbreviated string mnemonic. An FA2 contract client +(like another contract or a wallet) could use on-the-chain or off-the-chain registry +to map the error code mnemonic to a user-readable, localized message. A particular +implementation of the FA2 contract MAY extend the standard set of errors with custom +mnemonics for additional constraints. + +Standard error mnemonics: + +| Error mnemonic | Description | +| :------------- | :---------- | +| `"FA2_TOKEN_UNDEFINED"` | One of the specified `token_id`s is not defined within the FA2 contract | +| `"FA2_INSUFFICIENT_BALANCE"` | A token owner does not have sufficient balance to transfer tokens from owner's account| +| `"FA2_TX_DENIED"` | A transfer failed because of `operator_transfer_policy == No_transfer` | +| `"FA2_NOT_OWNER"` | A transfer failed because `operator_transfer_policy == Owner_transfer` and it is invoked not by the token owner | +| `"FA2_NOT_OPERATOR"` | A transfer failed because `operator_transfer_policy == Owner_or_operator_transfer` and it is invoked neither by the token owner nor a permitted operator | +| `"FA2_OPERATORS_UNSUPPORTED"` | `update_operators` entry point is invoked and `operator_transfer_policy` is `No_transfer` or `Owner_transfer` | +| `"FA2_RECEIVER_HOOK_FAILED"` | The receiver hook failed. This error MUST be raised by the hook implementation | +| `"FA2_SENDER_HOOK_FAILED"` | The sender failed. This error MUST be raised by the hook implementation | +| `"FA2_RECEIVER_HOOK_UNDEFINED"` | Receiver hook is required by the permission behavior, but is not implemented by a receiver contract | +| `"FA2_SENDER_HOOK_UNDEFINED"` | Sender hook is required by the permission behavior, but is not implemented by a sender contract | + +If more than one error conditions are met, the entry point MAY fail with any applicable +error. + +When error occurs, any FA2 contract entry point MUST fail with one of the following +types: + +1. `string` value which represents an error code mnemonic. +2. a Michelson `pair`, where the first element is a `string` representing error code +mnemonic and the second element is a custom error data. + +Some FA2 implementations MAY introduce their custom errors that MUST follow the +same pattern as standard ones. + +## Implementing Different Token Types With FA2 + +The FA2 interface is designed to support a wide range of token types and implementations. +This section gives examples of how different types of the FA2 contracts MAY be +implemented and what are the expected properties of such an implementation. + +### Single Fungible Token + +An FA2 contract represents a single token similar to ERC-20 or FA1.2 standards. + +| Property | Constrains | +| :--------------- | :--------: | +| `token_id` | Always `0n` | +| transfer amount | natural number | +| account balance | natural number | +| total supply | natural number | +| decimals | custom | + +### Multiple Fungible Tokens + +An FA2 contract may represent multiple tokens similar to ERC-1155 standard. +The implementation can have a fixed predefined set of supported tokens or tokens +can be created dynamically. + +| Property | Constrains | +| :--------------- | :--------: | +| `token_id` | natural number | +| transfer amount | natural number | +| account balance | natural number | +| total supply | natural number | +| decimals | custom, per each `token_id` | + +### Non-fungible Tokens + +An FA2 contract may represent non-fungible tokens (NFT) similar to ERC-721 standard. +For each individual non-fungible token the implementation assigns a unique `token_id`. +The implementation MAY support either a single kind of NFTs or multiple kinds. +If multiple kinds of NFT is supported, each kind MAY be assigned a continuous range +of natural number (that does not overlap with other ranges) and have its own associated +metadata. + +| Property | Constrains | +| :--------------- | :--------: | +| `token_id` | natural number | +| transfer amount | `0n` or `1n` | +| account balance | `0n` or `1n` | +| total supply | `0n` or `1n` | +| decimals | `0n` or a natural number if a token represents a batch of items | + +For any valid `token_id` only one account CAN hold the balance of one token (`1n`). +The rest of the accounts MUST hold zero balance (`0n`) for that `token_id`. + +### Mixing Fungible and Non-fungible Tokens + +An FA2 contract MAY mix multiple fungible and non-fungible tokens within the same +contract similar to ERC-1155. The implementation MAY chose to select individual +natural numbers to represent `token_id` for fungible tokens and continuous natural +number ranges to represent `token_id`s for NFTs. + +| Property | Constrains | +| :--------------- | :--------: | +| `token_id` | natural number | +| transfer amount | `0n` or `1n` for NFT and natural number for fungible tokens | +| account balance | `0n` or `1n` for NFT and natural number for fungible tokens | +| total supply | `0n` or `1n` for NFT and natural number for fungible tokens | +| decimals | custom | + +### Non-transferable Tokens + +Either fungible and non-fungible tokens can be non-transferable. Non-transferable +tokens can be represented by the FA2 contract which [operator transfer behavior](#operator-transfer-behavior) +is defined as `No_transfer`. Tokens cannot be transferred neither by the token owner +nor by any operator. Only privileged operations like mint and burn can assign tokens +to owner accounts. + +## Future Directions + +Future amendments to Tezos are likely to enable new functionality by which this +standard can be upgraded. Namely, [read-only +calls](https://forum.tezosagora.org/t/adding-read-only-calls/1227), event logging, +and [contract signatures](https://forum.tezosagora.org/t/contract-signatures/1458), now known as "tickets". + +## Copyright + +Copyright and related rights waived via +[CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution_expanded.cmd b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution_expanded.cmd new file mode 100644 index 0000000..337db01 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Camel/ChapterFA20Permission/solution_expanded.cmd @@ -0,0 +1,137 @@ +ligo compile-storage shiptoken.mligo main +'{ + paused=false; + entities=Map.literal [((1n,0n),{name="first";code="040233"})]; + entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; + owner_entities=Map.literal [ + (("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); + (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); + (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set)) + ]; + tokens=Map.literal [ + (0n,{ + total_supply=1n; + metadata={ + token_id=0n; + symbol="<3"; + name="TzAcademyShip"; + decimals=0n; + extras=(Map.empty :(string, string) map) + } + }) + ]; + operators=(Set.empty : operator_param set); + administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); + permissions_descriptor={ + operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); + receiver=Layout.convert_to_right_comb(Owner_no_hook); + sender=Layout.convert_to_right_comb(Owner_no_hook); + custom=(None : custom_permission_policy_michelson option) + } +}' + + +ligo dry-run --sender=tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J shiptoken.mligo main +'Fa2 ( + Update_operators([ + ( + Layout.convert_to_right_comb( + Add_operator( + ( + Layout.convert_to_right_comb( + ({ + owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address) + }: operator_param) + ) : operator_param_michelson + ) + ) + ) : update_operator_michelson) + ]) +)' +'{ + paused=false; + entities=Map.literal [((1n,0n),{name="first";code="040233"})]; + entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; + owner_entities=Map.literal [ + (("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); + (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); + (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set)) + ]; + tokens=Map.literal [ + (0n,{ + total_supply=1n; + metadata={ + token_id=0n; + symbol="<3"; + name="TzAcademyShip"; + decimals=0n; + extras=(Map.empty :(string, string) map) + } + }) + ]; + operators=(Set.empty : operator_param set); + administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); + permissions_descriptor={ + operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); + receiver=Layout.convert_to_right_comb(Owner_no_hook); + sender=Layout.convert_to_right_comb(Owner_no_hook); + custom=(None : custom_permission_policy_michelson option) + } +}' + + +ligo dry-run --sender=tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU shiptoken.mligo main +'Fa2 ( + Transfer( [ + (Layout.convert_to_right_comb( + ({ + from_=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + txs=[ + (Layout.convert_to_right_comb( + ({ + to_=("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN": address); + token_id=0n; + amount=1n + }: transfer_destination) + ) : transfer_destination_michelson) + ] + } : transfer_aux) + ): transfer_michelson) + ]) +)' +'{ + paused=false; + entities=Map.literal [((1n,0n),{name="first";code="040233"})]; + entity_owner=Map.literal [((1n,0n),("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address))]; + owner_entities=Map.literal [ + (("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address), (Set.add (1n,0n) (Set.empty:entity_key set))); + (("tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN":address),(Set.empty:entity_key set)); + (("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address),(Set.empty:entity_key set)) + ]; + tokens=Map.literal [ + (0n,{ + total_supply=1n; + metadata={ + token_id=0n; + symbol="<3"; + name="TzAcademyShip"; + decimals=0n; + extras=(Map.empty :(string, string) map) + } + }) + ]; + operators=Set.add + ({ + owner=("tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv":address); + operator=("tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU":address) + }: operator_param) + (Set.empty : operator_param set); + administrator=("tz1UK81V9ccgpDjq8MVUE9uP4mnmNiSZQm9J" : address); + permissions_descriptor={ + operator=Layout.convert_to_right_comb(Owner_or_operator_transfer); + receiver=Layout.convert_to_right_comb(Owner_no_hook); + sender=Layout.convert_to_right_comb(Owner_no_hook); + custom=(None : custom_permission_policy_michelson option) + } +}' From 83ded2cb06b5310fabef2090dcdae6303e4f7bd5 Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Wed, 24 Jun 2020 16:01:37 +0200 Subject: [PATCH 15/16] merge with master --- src/frontend/src/pages/Chapter/Chapter.data.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/frontend/src/pages/Chapter/Chapter.data.tsx b/src/frontend/src/pages/Chapter/Chapter.data.tsx index 6d575fd..6fe2fd6 100644 --- a/src/frontend/src/pages/Chapter/Chapter.data.tsx +++ b/src/frontend/src/pages/Chapter/Chapter.data.tsx @@ -70,6 +70,7 @@ import { data as reasonDataLoops } from "../Chapters/Reason/ChapterLoops"; import { data as reasonDataMainFunction } from "../Chapters/Reason/ChapterMainFunction"; import { data as reasonDataMaps } from "../Chapters/Reason/ChapterMaps"; import { data as reasonDataMath } from "../Chapters/Reason/ChapterMath"; +import { data as reasonDataMultisig } from "../Chapters/Reason/ChapterMultisig"; import { data as reasonDataOption } from "../Chapters/Reason/ChapterOption"; import { data as reasonDataPolymorphism } from "../Chapters/Reason/ChapterPolymorphism"; import { data as reasonDataRecords } from "../Chapters/Reason/ChapterRecords"; @@ -83,10 +84,6 @@ import { data as reasonDataVariant } from "../Chapters/Reason/ChapterVariant"; - -import { data as reasonDataMultisig } from "../Chapters/Reason/ChapterMultisig"; - - export const chapterData = [ { pathname: '/pascal/chapter-about', From cc7ae205b60f2b532b36de8f57dedf26ba5aae6d Mon Sep 17 00:00:00 2001 From: Frank Hillard Date: Wed, 24 Jun 2020 18:26:45 +0200 Subject: [PATCH 16/16] fix extension exercise.religo --- src/frontend/package.json | 2 +- .../src/pages/Chapters/Pascal/ChapterInterop/index.ts | 4 ++-- .../Reason/ChapterInterop/{exercise.mligo => exercise.religo} | 0 .../Reason/ChapterInterop/{solution.mligo => solution.religo} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/frontend/src/pages/Chapters/Reason/ChapterInterop/{exercise.mligo => exercise.religo} (100%) rename src/frontend/src/pages/Chapters/Reason/ChapterInterop/{solution.mligo => solution.religo} (100%) diff --git a/src/frontend/package.json b/src/frontend/package.json index bee1575..26e4f5e 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -73,7 +73,7 @@ "babel-plugin-styled-components": "^1.10.7", "cypress": "^4.7.0", "eslint-config-react-app": "^5.2.1", - "jest": "^26.0.1", + "jest": "^24.9.0", "mockdate": "^3.0.2", "raw-loader": "^4.0.1", "react-test-renderer": "^16.13.1", diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterInterop/index.ts b/src/frontend/src/pages/Chapters/Pascal/ChapterInterop/index.ts index 5ba0a9f..5afd766 100644 --- a/src/frontend/src/pages/Chapters/Pascal/ChapterInterop/index.ts +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterInterop/index.ts @@ -3,9 +3,9 @@ import course from "!raw-loader!./course.md"; /* eslint import/no-webpack-loader-syntax: off */ // @ts-ignore -import exercise from "!raw-loader!./exercise.ligo"; +import exercise from "!raw-loader!./exercise.mligo"; /* eslint import/no-webpack-loader-syntax: off */ // @ts-ignore -import solution from "!raw-loader!./solution.ligo"; +import solution from "!raw-loader!./solution.mligo"; export const data = { course, exercise, solution }; diff --git a/src/frontend/src/pages/Chapters/Reason/ChapterInterop/exercise.mligo b/src/frontend/src/pages/Chapters/Reason/ChapterInterop/exercise.religo similarity index 100% rename from src/frontend/src/pages/Chapters/Reason/ChapterInterop/exercise.mligo rename to src/frontend/src/pages/Chapters/Reason/ChapterInterop/exercise.religo diff --git a/src/frontend/src/pages/Chapters/Reason/ChapterInterop/solution.mligo b/src/frontend/src/pages/Chapters/Reason/ChapterInterop/solution.religo similarity index 100% rename from src/frontend/src/pages/Chapters/Reason/ChapterInterop/solution.mligo rename to src/frontend/src/pages/Chapters/Reason/ChapterInterop/solution.religo