diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/course.md b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/course.md new file mode 100644 index 0000000..17cbeb8 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/course.md @@ -0,0 +1,241 @@ +# Chapter 30 : 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. + +Although, it is recommended to implement "transfer hook design pattern" in many cases this pattern is prohibitively expensive in terms of gas cost due to extra inter-contract calls. + +#### 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*). + +![](/images/small-fa2-hook.png) + +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 is record [ + hook : (unit) -> contract(transfer_descriptor_param); + permissions_descriptor : permissions_descriptor_; +] + +type set_hook_param_aux is record [ + hook : (unit) -> contract(transfer_descriptor_param); + permissions_descriptor : permissions_descriptor; +] + +type set_hook_param_michelson is michelson_pair_right_comb(set_hook_param_aux) + +type fa2_with_hook_entry_points is + 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 + +##### 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 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). + + +##### 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. + + + + +//// NOT IMPLEMENTED START/// +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 +``` +//// NOT IMPLEMENTED END/// + + +#### 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 hook permission contract + +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. + + +``` +(** +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 "tzip-12/lib/fa2_transfer_hook_lib.ligo" +#include "tzip-12/lib/fa2_hooks_lib.ligo" + +type storage is record [ + fa2_registry : fa2_registry; + descriptor : permissions_descriptor; +] + +type entry_points is + | Tokens_transferred_hook of transfer_descriptor_param + | Register_with_fa2 of contract(fa2_with_hook_entry_points) + +function tokens_transferred_hook(const pm : transfer_descriptor_param; const s : storage) : list(operation) * storage is +block { + const p : transfer_descriptor_param_ = Layout.convert_to_right_comb (pm); + const u : unit = validate_hook_call (Tezos.sender, s.fa2_registry); + const ops : list(operation) = owners_transfer_hook(record [ligo_param = p; michelson_param = pm], s.descriptor); +} with (ops, s) + +function register(const fa2 : contract(fa2_with_hook_entry_points); const s : storage) : list(operation) * storage is +block { + const ret : list(operation) * set(address) = register_with_fa2 (fa2, s.descriptor, s.fa2_registry); + s.fa2_registry := ret.1; +} with (list [ret.0], s) + +function main (const param : entry_points; const s : storage) : list(operation) * storage is + block { skip } with + case param of + | Tokens_transferred_hook (pm) -> tokens_transferred_hook(pm, s) + | Register_with_fa2 (fa2) -> register(fa2, s.descriptor, s.fa2_registry) +end + +(** example policies *) + +(* the policy which allows only token owners to transfer their own tokens. *) +const own_policy : permissions_descriptor = record [ + operator = Layout.convert_to_right_comb(Owner_transfer); + sender = Layout.convert_to_right_comb(Owner_no_hook); + receiver = Layout.convert_to_right_comb(Owner_no_hook); + custom = (None : option(custom_permission_policy)); +] +``` + +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. + +![](/images/small-fa2-hook-exercise.png) + +Rule 1 - we want to accept a transfer if transfer receiver is registered in a whitelist. This whitelisting is done via a tranfer hook. + +Rule 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. + +- As you can see the function *check\_receiver* verifies if a receiver _r_ implements *fa2\_token\_receiver* interface, using *to\_receiver\_hook* function and a _case_ operator. If the receiver _r_ implements *fa2\_token\_receiver* interface, the function *create\_hook\_receiver\_operation* is called with _h_ as hook entry point. + + +1- Prepare parameter - cast parameter _p_ into type *transfer\_descriptor\_param* and store the result in a new variable _pm_. You can check the *fa2\_interface.ligo* for type definition of *transfer\_descriptor\_param* and use the *Layout.convert\_to\_right\_comb* function for conversion. + +2- Call the entry point - create a variable _op_ of type *operation* which is a transaction sending variable _pm_ and no mutez to the retrieved hook entry point _h_ + +3- Return transactions - add this newly created operation _op_ in the returned list of operation _ops_ and return it. + +- if the receiver _r_ does not implement *fa2\_token\_receiver* interface, the function *verify\_receiver\_in\_whitelist* is called. Modify function *verify\_receiver\_in\_whitelist* with following implementation requirements: + +4- Check if receiver _r_ is registered in the whitelist _wl_. + +5- If it is the case , everything is fine, just return the returned list of operation _ops_. + +6- Otherwise throw an exception with "Not in whitelist" message. Don't forget to cast the exception. + diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/exercise.ligo b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/exercise.ligo new file mode 100644 index 0000000..eb08a7d --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/exercise.ligo @@ -0,0 +1,103 @@ +#include "tzip-12/lib/fa2_transfer_hook_lib.ligo" +#include "tzip-12/lib/fa2_hooks_lib.ligo" + +type storage is record [ + fa2_registry : fa2_registry; + receiver_whitelist : set(address); +] + +function custom_validate_receivers (const p : transfer_descriptor_param_; const wl : set(address)) : list(operation) is +block { + function convert_get(const tm : transfer_destination_descriptor) : option(address) is block { + const t : transfer_destination_descriptor_ = Layout.convert_from_right_comb((tm:transfer_destination_descriptor)) + } with t.to_; + + function get_receiver(const txm : transfer_descriptor) : list(option(address)) is block { + const tx : transfer_descriptor_ = Layout.convert_from_right_comb(txm); + } with List.map( convert_get, tx.txs); + + const receivers : set(address) = get_owners_from_batch (p.batch, get_receiver); + + function create_hook_receiver_operation(const ops : list(operation); const h : contract(transfer_descriptor_param); const p : transfer_descriptor_param_) : list(operation) is + // Type your solution below + block { + + } with ; + + function verify_receiver_in_whitelist(const ops : list(operation); const r : address; const wl : set(address)) : list(operation) is + // Type your solution below + + + (* receiver contract implements fa2_token_receiver interface: invoke it otherwise check whitelist*) + function check_receiver (const ops : list(operation); const r : address) : list(operation) is + case to_receiver_hook(r) of + | Some (h) -> create_hook_receiver_operation(ops, h, p) + | None -> verify_receiver_in_whitelist(ops, r, wl) + end + +} with Set.fold(check_receiver, receivers, (nil : list(operation))) + +function custom_transfer_hook (const p : transfer_descriptor_param_; const s : storage) : list(operation) is + custom_validate_receivers (p, s.receiver_whitelist) + + +function get_policy_descriptor (const u : unit) : permissions_descriptor_ is +block { skip } with record [ + operator = Layout.convert_to_right_comb(Owner_or_operator_transfer); + sender = Layout.convert_to_right_comb(Owner_no_hook); + receiver = Layout.convert_to_right_comb(Owner_no_hook) ; (* overridden by the custom policy *) + custom = Some (Layout.convert_to_right_comb((record [ + tag = "receiver_hook_and_whitelist"; + config_api = Some (Tezos.self_address); //(None: option(address)); + ]: custom_permission_policy_))); +] + +type config_whitelist is + | Add_receiver_to_whitelist of set(address) + | Remove_receiver_from_whitelist of set(address) + +function configure_receiver_whitelist (const cfg : config_whitelist; const wl : set(address)) : set(address) is +block { skip } with case cfg of + | Add_receiver_to_whitelist (rs) -> + Set.fold ( + (function (const l : set(address); const a : address) : set(address) is Set.add (a, l) ), + rs, wl) + | Remove_receiver_from_whitelist (rs) -> + Set.fold ( + (function (const l : set(address); const a : address) : set(address) is Set.remove (a, l) ), + rs, + wl) + end + + +type entry_points is + | Tokens_transferred_hook of transfer_descriptor_param_ + | Register_with_fa2 of contract(fa2_with_hook_entry_points) + | Config_receiver_whitelist of config_whitelist + +function tokens_transferred_hook(const p : transfer_descriptor_param_; const s : storage) : list(operation) * storage is +block { + const u : unit = validate_hook_call (Tezos.sender, s.fa2_registry); + const ops : list(operation) = custom_transfer_hook (p, s); +} with (ops, s) + +function register_with_fa2(const fa2 : contract(fa2_with_hook_entry_points); const s : storage) : list(operation) * storage is +block { + const descriptor : permissions_descriptor_ = get_policy_descriptor (unit); + const ret : operation * fa2_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry); + s.fa2_registry := ret.1; +} with (list [ret.0], s) + +function config_receiver_whitelist(const cfg : config_whitelist; const s : storage) : list(operation) * storage is +block { + const new_wl : set(address) = configure_receiver_whitelist (cfg, s.receiver_whitelist); + s.receiver_whitelist := new_wl; +} with ((nil : list(operation)), s) + +function main (const param : entry_points; const s : storage) : list(operation) * storage is +block { skip } with + case param of + | Tokens_transferred_hook (p) -> tokens_transferred_hook(p,s) + | Register_with_fa2 (fa2) -> register_with_fa2(fa2, s) + | Config_receiver_whitelist (cfg) -> config_receiver_whitelist(cfg, s) + end \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/index.ts b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/index.ts new file mode 100644 index 0000000..87f2796 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/index.ts @@ -0,0 +1,26 @@ +/* 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.ligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import solution from "!raw-loader!./solution.ligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support1 from "!raw-loader!./tzip-12/lib/fa2_transfer_hook_lib.ligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support2 from "!raw-loader!./tzip-12/lib/fa2_operator_lib.ligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support3 from "!raw-loader!./tzip-12/lib/fa2_hooks_lib.ligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support4 from "!raw-loader!./tzip-12/fa2_interface.ligo"; +/* eslint import/no-webpack-loader-syntax: off */ +// @ts-ignore +import support5 from "!raw-loader!./tzip-12/fa2_hook.ligo"; + +export const data = { course, exercise, solution, support1, support2, support3, support4, support5 }; diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/solution.ligo b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/solution.ligo new file mode 100644 index 0000000..25835fe --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/solution.ligo @@ -0,0 +1,106 @@ +#include "tzip-12/lib/fa2_transfer_hook_lib.ligo" +#include "tzip-12/lib/fa2_hooks_lib.ligo" + +type storage is record [ + fa2_registry : fa2_registry; + receiver_whitelist : set(address); +] + +function custom_validate_receivers (const p : transfer_descriptor_param_; const wl : set(address)) : list(operation) is +block { + function convert_get(const tm : transfer_destination_descriptor) : option(address) is block { + const t : transfer_destination_descriptor_ = Layout.convert_from_right_comb((tm:transfer_destination_descriptor)) + } with t.to_; + + function get_receiver(const txm : transfer_descriptor) : list(option(address)) is block { + const tx : transfer_descriptor_ = Layout.convert_from_right_comb(txm); + } with List.map( convert_get, tx.txs); + + const receivers : set(address) = get_owners_from_batch (p.batch, get_receiver); + + function create_hook_receiver_operation(const ops : list(operation); const h : contract(transfer_descriptor_param); const p : transfer_descriptor_param_) : list(operation) is + // Type your solution below + block { + const pm : transfer_descriptor_param = Layout.convert_to_right_comb (p); + const op : operation = Tezos.transaction(pm, 0mutez, h); + } with op # ops; + + function verify_receiver_in_whitelist(const ops : list(operation); const r : address; const wl : set(address)) : list(operation) is + // Type your solution below + if Set.mem (r, wl) + then ops + else (failwith ("Not in whitelist") : list(operation)); + + (* receiver contract implements fa2_token_receiver interface: invoke it otherwise check whitelist*) + function check_receiver (const ops : list(operation); const r : address) : list(operation) is + case to_receiver_hook(r) of + | Some (h) -> create_hook_receiver_operation(ops, h, p) + | None -> verify_receiver_in_whitelist(ops, r, wl) + end + +} with Set.fold(check_receiver, receivers, (nil : list(operation))) + +function custom_transfer_hook (const p : transfer_descriptor_param_; const s : storage) : list(operation) is + custom_validate_receivers (p, s.receiver_whitelist) + + +function get_policy_descriptor (const u : unit) : permissions_descriptor_ is +block { skip } with record [ + operator = Layout.convert_to_right_comb(Owner_or_operator_transfer); + sender = Layout.convert_to_right_comb(Owner_no_hook); + receiver = Layout.convert_to_right_comb(Owner_no_hook) ; (* overridden by the custom policy *) + custom = Some (Layout.convert_to_right_comb((record [ + tag = "receiver_hook_and_whitelist"; + config_api = Some (Tezos.self_address); //(None: option(address)); + ]: custom_permission_policy_))); +] + +type config_whitelist is + | Add_receiver_to_whitelist of set(address) + | Remove_receiver_from_whitelist of set(address) + +function configure_receiver_whitelist (const cfg : config_whitelist; const wl : set(address)) : set(address) is +block { skip } with case cfg of + | Add_receiver_to_whitelist (rs) -> + Set.fold ( + (function (const l : set(address); const a : address) : set(address) is Set.add (a, l) ), + rs, wl) + | Remove_receiver_from_whitelist (rs) -> + Set.fold ( + (function (const l : set(address); const a : address) : set(address) is Set.remove (a, l) ), + rs, + wl) + end + + +type entry_points is + | Tokens_transferred_hook of transfer_descriptor_param_ + | Register_with_fa2 of contract(fa2_with_hook_entry_points) + | Config_receiver_whitelist of config_whitelist + +function tokens_transferred_hook(const p : transfer_descriptor_param_; const s : storage) : list(operation) * storage is +block { + const u : unit = validate_hook_call (Tezos.sender, s.fa2_registry); + const ops : list(operation) = custom_transfer_hook (p, s); +} with (ops, s) + +function register_with_fa2(const fa2 : contract(fa2_with_hook_entry_points); const s : storage) : list(operation) * storage is +block { + const descriptor : permissions_descriptor_ = get_policy_descriptor (unit); + const ret : operation * fa2_registry = register_with_fa2 (fa2, descriptor, s.fa2_registry); + s.fa2_registry := ret.1; +} with (list [ret.0], s) + +function config_receiver_whitelist(const cfg : config_whitelist; const s : storage) : list(operation) * storage is +block { + const new_wl : set(address) = configure_receiver_whitelist (cfg, s.receiver_whitelist); + s.receiver_whitelist := new_wl; +} with ((nil : list(operation)), s) + +function main (const param : entry_points; const s : storage) : list(operation) * storage is +block { skip } with + case param of + | Tokens_transferred_hook (p) -> tokens_transferred_hook(p,s) + | Register_with_fa2 (fa2) -> register_with_fa2(fa2, s) + | Config_receiver_whitelist (cfg) -> config_receiver_whitelist(cfg, s) + end \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/fa2_hook.ligo b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/fa2_hook.ligo new file mode 100644 index 0000000..d360843 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/fa2_hook.ligo @@ -0,0 +1,22 @@ + +#if !FA2_HOOK +#define FA2_HOOK + +#include "fa2_interface.ligo" + +type set_hook_param is record [ + hook : (unit) -> contract(transfer_descriptor_param); + permissions_descriptor : permissions_descriptor_; +] + +type set_hook_param_aux is record [ + hook : (unit) -> contract(transfer_descriptor_param); + permissions_descriptor : permissions_descriptor; +] + +type set_hook_param_michelson is michelson_pair_right_comb(set_hook_param_aux) + +type fa2_with_hook_entry_points is + Set_transfer_hook of set_hook_param_michelson + +#endif diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/fa2_interface.ligo b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/fa2_interface.ligo new file mode 100644 index 0000000..d756442 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/fa2_interface.ligo @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: 2020 tqtezos +// SPDX-License-Identifier: MIT + +#if ! FA2_INTERFACE +#define FA2_INTERFACE + +type token_id is nat + +const default_token_id : token_id = 0n; + +(* + * This function fails if provided token_id is not equal to + * default one, restricting all operations to be one-token + * (that are allowed for `default_token_id`) + *) +function validate_token_type + ( const token_id : token_id + ) : unit is + if token_id =/= default_token_id + then failwith ("FA2_TOKEN_UNDEFINED") + else unit + +(* + * Same as above but for a list of token ids + *) +function validate_token_types + ( const token_ids : list (token_id) + ) : unit is List.fold + ( function + ( const u : unit + ; const token_id : token_id + ) : unit is validate_token_type (token_id) + , token_ids + , unit + ) + + +type transfer_destination_ is record + to_ : address +; token_id : token_id +; amount : nat +end + +type transfer_destination is michelson_pair_right_comb(transfer_destination_) + +type transfer_param_ is record + from_ : address +; txs : list (transfer_destination) +end + +type transfer_param is michelson_pair_right_comb(transfer_param_) + +type transfer_params is list (transfer_param) + +type balance_of_request is record + owner : address +; token_id : token_id +end + +type balance_of_response_ is record + request : balance_of_request +; balance : nat +end + +type balance_of_response is michelson_pair_right_comb(balance_of_response_) + +type balance_of_params_ is record + requests : list (balance_of_request) +; callback : contract (list (balance_of_response)) +end + +type balance_of_params is michelson_pair_right_comb(balance_of_params_) + +type token_metadata_ is record + token_id : token_id +; symbol : string +; name : string +; decimals : nat +; extras : map (string, string) +end + +type token_metadata is michelson_pair_right_comb(token_metadata_) + +type token_metadata_registry_params is contract (address) + +type owner is address +type operator is address + +type operators is + big_map ((owner * operator), unit) + +type operator_param_ is record + owner : address +; operator : address +end + +type operator_param is michelson_pair_right_comb(operator_param_) + +type update_operator_param is +| Add_operator of operator_param +| Remove_operator of operator_param + +type update_operator_params is list (update_operator_param) + +type is_operator_response_ is record + operator : operator_param +; is_operator : bool +end + +type is_operator_response is michelson_pair_right_comb(is_operator_response_) + +type is_operator_params_ is record + operator : operator_param +; callback : contract (is_operator_response) +end + +type is_operator_params is michelson_pair_right_comb(is_operator_params_) + +(* ------------------------------------------------------------- *) + +type operator_transfer_policy_ is +| No_transfer +| Owner_transfer +| Owner_or_operator_transfer + +type operator_transfer_policy is michelson_or_right_comb(operator_transfer_policy_) + +type owner_hook_policy_ is +| Owner_no_hook +| Optional_owner_hook +| Required_owner_hook + +type owner_hook_policy is michelson_or_right_comb(owner_hook_policy_) + +type custom_permission_policy_ is record + tag : string +; config_api : option (address) +end + +type custom_permission_policy is michelson_pair_right_comb(custom_permission_policy_) + +type permissions_descriptor_ is record + operator : operator_transfer_policy +; receiver : owner_hook_policy +; sender : owner_hook_policy +; custom : option (custom_permission_policy) +end + +type permissions_descriptor is michelson_pair_right_comb(permissions_descriptor_) + +type permissions_descriptor_params is + contract (permissions_descriptor) + + +type fa2_entry_points is + Transfer of transfer_params +| Balance_of of balance_of_params +| Token_metadata_registry of token_metadata_registry_params +| Permissions_descriptor of permissions_descriptor_params +| Update_operators of update_operator_params +| Is_operator of is_operator_params + +(* ------------------------------------------------------------- *) + +(* + * Hooks + *) + +type transfer_destination_descriptor_ is record + to_ : option (address) +; token_id : token_id +; amount : nat +end + +type transfer_destination_descriptor is michelson_pair_right_comb(transfer_destination_descriptor_) + +type transfer_descriptor_ is record + from_ : option (address) +; txs : list (transfer_destination_descriptor) +end + +type transfer_descriptor is michelson_pair_right_comb(transfer_descriptor_) + +type transfer_descriptor_param_ is record + fa2 : address +; batch : list(transfer_descriptor) +; operator : address +end + +type transfer_descriptor_param is michelson_pair_right_comb(transfer_descriptor_param_) + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_hooks_lib.ligo b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_hooks_lib.ligo new file mode 100644 index 0000000..202e9b6 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_hooks_lib.ligo @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2020 tqtezos +// SPDX-License-Identifier: MIT + +#if !FA2_BEHAVIORS +#define FA2_BEHAVIORS + +#include "../fa2_interface.ligo" + +(* ------------------------------------------------------------- *) + +type get_owners is transfer_descriptor -> list(option(address)) + + +type to_hook is address -> option (contract (transfer_descriptor_param)) + + +function get_owners_from_batch (const batch : list(transfer_descriptor); const get_owners : get_owners) : set(address) is block { + function apply_get_owners(const acc : set(address); const tx : transfer_descriptor) : set(address) is block { + const owners : list(option(address)) = get_owners (tx); + } with List.fold( + (function (const acc : set(address); const o: option(address)) : set(address) is case o of + | None -> acc + | Some (a) -> Set.add (a, acc) + end + ), + owners, + acc + ) +} with List.fold(apply_get_owners, batch, (Set.empty : set(address))) + +(* + * Helper function that converts `transfer_param` to `transfer_descriptor_param` + *) +function convert_to_transfer_descriptor + ( const self_addr : address + ; const param_sender_addr : address + ; const transfer_destination : transfer_destination + ): transfer_descriptor_param is block +{ const transfer_destination_descriptor_ : transfer_destination_descriptor_ = record + [ to_ = Some (transfer_destination.0) + ; token_id = transfer_destination.1.0 + ; amount = transfer_destination.1.1 + ] +; const transfer_destination_descriptor : transfer_destination_descriptor = + Layout.convert_to_right_comb ((transfer_destination_descriptor_ : transfer_destination_descriptor_)) +; const transfer_descriptor_ : transfer_descriptor_ = record + [ from_ = Some (param_sender_addr) + ; txs = list [ transfer_destination_descriptor ] + ] +; const transfer_descriptor_batch : list (transfer_descriptor) = list + [ Layout.convert_to_right_comb ((transfer_descriptor_ : transfer_descriptor_)) ] +; const transfer_descriptor_param : transfer_descriptor_param_ = record + [ fa2 = self_addr + ; batch = transfer_descriptor_batch + ; operator = Tezos.sender + ] +} with Layout.convert_to_right_comb ((transfer_descriptor_param : transfer_descriptor_param_)) + +(* + * Append a transaction of transfer hook call + * to a list of operations provided + *) +function validate_owner_hook( const self_addr : address; const param_sender_addr : address; const is_sender : bool; const ops : list (operation); const transfer_destination : transfer_destination +; const to_hook : to_hook) : list(operation) is block +{ + const hook_addr : address = if is_sender then param_sender_addr else transfer_destination.0; +} with case to_hook (hook_addr) of + Some (hook) -> + Tezos.transaction (convert_to_transfer_descriptor(self_addr, param_sender_addr, transfer_destination), 0mutez, hook) # ops + | None -> ops + end + +(* + * Retrieves contract from `tokens_received` entrypoint + *) +function to_receiver_hook( const receiving_address : address ) : option (contract (transfer_descriptor_param)) is + Tezos.get_entrypoint_opt ("%tokens_received", receiving_address) + +(* + * Retrieves contract from `tokens_sent` entrypoint + *) +function to_sender_hook( const sender_address : address) : option (contract (transfer_descriptor_param)) is + Tezos.get_entrypoint_opt ("%tokens_sent", sender_address) + +(* + * Make a list of transfer hook calls for each sender or receiver + *) +function validate_owner_hooks( const params : transfer_param; const self_addr : address; const is_sender : bool) : list (operation) is + block { + const to_hook : to_hook = if is_sender then to_sender_hook else to_receiver_hook; + + function validate( const ops : list (operation); const td : transfer_destination) : list (operation) is + block { + const owner : address = if is_sender then params.0 else td.0 + } with validate_owner_hook (self_addr, params.0, is_sender, ops, td, to_hook) + +} with List.fold (validate, params.1, (nil : list (operation))) + +function merge_operations + ( const fst : list (operation) + ; const snd : list (operation) + ) : list (operation) is List.fold + ( function + ( const operations : list (operation) + ; const operation : operation + ) : list (operation) is operation # operations + , fst + , snd + ) + +(* + * Construct a list of transfer hook calls for each sender and + * receiver if such were specified + *) +function generic_transfer_hook( const transfer_param : transfer_param) : list (operation) is +block { + const self_addr : address = Tezos.self_address; + const sender_ops : list (operation) = validate_owner_hooks (transfer_param, self_addr, True); + const receiver_ops : list (operation) = validate_owner_hooks (transfer_param, self_addr, False); +} with merge_operations (receiver_ops, sender_ops) + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.ligo b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.ligo new file mode 100644 index 0000000..5da3fda --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_operator_lib.ligo @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2020 tqtezos +// SPDX-License-Identifier: MIT + +(* + * Operator library for stablecoin smart-contract + *) + +#if !FA2_OPERATOR_LIB +#define FA2_OPERATOR_LIB + +#include "../fa2_interface.ligo" +#include "fa2_hooks_lib.ligo" + +(* + * Validates whether the given operator parameter is equal to `SENDER` + *) +function validate_operator_owner_is_sender + ( const param : operator_param + ) : unit is if param.0 = Tezos.sender + then unit else failwith ("NOT_TOKEN_OWNER") + + + +(* + * Validates operators for the given tranfser batch + * and operator storage + *) +function validate_operators + ( const transfer_param : transfer_param + ; const operators : operators + ) : unit is block +{ const operator : address = Tezos.sender +; const owner : address = transfer_param.0 +} with if owner = operator or Big_map.mem ((owner, operator), operators) + then unit else failwith ("FA2_NOT_OPERATOR") + +(* + * Add operator from the given parameter and operator storage + *) +function add_operator + ( const param : operator_param + ; const operators : operators + ) : operators is block +{ validate_operator_owner_is_sender (param) +; const operator_key : (owner * operator) = (param.0, param.1) +} with Big_map.update (operator_key, Some (unit), operators) + +(* + * Remove operator from the given storage + *) +function remove_operator + ( const param : operator_param + ; const operators : operators + ) : operators is block +{ validate_operator_owner_is_sender (param) +; const operator_key : (owner * operator) = (param.0, param.1) +} with Big_map.remove (operator_key, operators) + +(* + * Adds or removes operators from a provided list of instructions + * accordingly for the given storage + *) +function update_operators + ( const params : update_operator_params + ; const operators : operators + ) : operators is List.fold + ( function + ( const operators : operators + ; const update_operator_param : update_operator_param + ) : operators is case update_operator_param of + Add_operator (param) -> add_operator (param, operators) + | Remove_operator (param) -> remove_operator (param, operators) + end + , params + , operators + ) + +#endif \ No newline at end of file diff --git a/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.ligo b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.ligo new file mode 100644 index 0000000..da2ed12 --- /dev/null +++ b/src/frontend/src/pages/Chapters/Pascal/ChapterFA20Hook/tzip-12/lib/fa2_transfer_hook_lib.ligo @@ -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.ligo" + +function get_hook_entrypoint (const hook_contract : address) : (unit) -> contract(transfer_descriptor_param) is +block { + const hook_entry_opt : option(contract(transfer_descriptor_param)) = (None : option(contract(transfer_descriptor_param)) ); //Tezos.get_entrypoint_opt("%tokens_transferred_hook", hook_contract); + const hook_entry : contract(transfer_descriptor_param) = case (hook_entry_opt) of + | Some (hook_entry) -> hook_entry + | None -> (failwith("Undefined hook"): contract(transfer_descriptor_param)) + end +} with (function (const u : unit) : contract(transfer_descriptor_param) is hook_entry) + +function create_register_hook_op(const fa2 : contract(fa2_with_hook_entry_points); const descriptor : permissions_descriptor_) : operation is +block { + const hook_fn : (unit) -> contract(transfer_descriptor_param) = get_hook_entrypoint(Tezos.self_address); + const p : set_hook_param_aux = record [ + hook = hook_fn; + permissions_descriptor = Layout.convert_to_right_comb(descriptor); + ]; + const pm : set_hook_param_michelson = Layout.convert_to_right_comb((p:set_hook_param_aux)); + const op : operation = Tezos.transaction (Set_transfer_hook (pm), 0mutez, fa2) +} with op + +type fa2_registry is set(address) + +function register_with_fa2 (const fa2 : contract(fa2_with_hook_entry_points); const descriptor : permissions_descriptor_; const registry : fa2_registry) : operation * fa2_registry is +block { + const op : operation = create_register_hook_op (fa2, descriptor); + const fa2_address : address = Tezos.address (fa2); + const new_registry : fa2_registry = Set.add (fa2_address, registry); +} with (op, new_registry) + +function validate_hook_call (const fa2 : address; const registry : fa2_registry) : unit is + if Set.mem (fa2, registry) + then unit + else failwith ("UNKNOWN_FA2_CALL") + +#endif