From 0ec27f4a045c6f56a818cf3b78ae32a4ae84dcdd Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 5 Mar 2021 15:29:02 -0500 Subject: [PATCH 01/18] draft of generic fractional ownership DAO --- fractional/generic_fractional/README.md | 76 +++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 fractional/generic_fractional/README.md diff --git a/fractional/generic_fractional/README.md b/fractional/generic_fractional/README.md new file mode 100644 index 0000000..fbec684 --- /dev/null +++ b/fractional/generic_fractional/README.md @@ -0,0 +1,76 @@ +# Generic Fractional Ownership DAO + +Existing [fractional ownership DAO](../fractional/README.md) controls FA2 token +transfer operation using fractional ownership voting. However, token management +may involve other operations with the token (like putting it for sale on some +marketplace or auction contract). The set of operations, that can be performed +with the NFT and require fractional ownership control, is not predefined and can +be extended in the future. + +The proposed generic DAO uses fractional voting to control any generic operation +represented by a lambda function. Such lambda can transfer tokens, buy/sell tokens +on market place or auction or perform any other generic operation. Strictly speaking, +generic fractional ownership DAO is not explicitly tied to any NFTs. Fractional +owners can vote on any lambda representing the operation; if they try to transfer +some token which is not owned by the DAO, such operation will fail, when the +transfer is validated by FA2 contract. + +## Entities + +**NFT token** - any generic implementation of an FA2 NFT token that can be +transferred between any addresses. + +**Ownership DAO** - a contract that can own any generic NFT token and control +any operation with owned NFTs using fractional ownership logic. + +**Ownership token** - a fungible FA2 token embedded within the ownership DAO. +Token balances are allocated to fractional owners. Such allocations can be changed +by transferring ownership tokens. + +**Fractional owner** - an address which owns a fraction of all NFTs managed by +the DAO. Fraction of ownership is represented by the balance of the ownership token +allocated to the fractional owner. Fractional owner can vote on any operation lambda +that manipulates managed NFTs. + +**Operation lambda** - any operation that manipulates owned NFT tokens or DAO itself. +Fractional owners can vote on execution of the operation lambda. + +## DAO operations + +**Transfer ownership tokens** - Since the ownership token managed by the DAO is +a regular FA2 fungible token, fractional owners can transfer it using standard +FA2 transfer. + +**Vote on operation lambda** - any fractional owner can submit a vote consisting +of a lambda (`unit -> operation`) and a nonce. +The vote weight is proportional to a balance of the ownership token allocated +to a fractional owner. Once predefined voting threshold is met, DAO executes the +lambda and returns produced operation. + +## DAO Entry Points + +### Standard FA2 entry points for ownership token + +`%transfer` + +`balance_of` + +`update_operators` + +### `%vote` + +```ocaml +%vote { + lambda: unit -> operation; + nonce: nat; +} +``` + +## Miscellaneous + +Providing lambda "templates" or some high-level client API to create and sign +lambda functions for generic operations should be considered. + +1. NFT `transfer` and `update_operators`. +2. Interaction with market place and auction contracts. +3. DAO administration. From 93bf2991b7f2622dcb21ba38ee6734086bc1a54b Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 5 Mar 2021 16:21:31 -0500 Subject: [PATCH 02/18] operation lambda can return a list of operations --- fractional/generic_fractional/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fractional/generic_fractional/README.md b/fractional/generic_fractional/README.md index fbec684..62f24bf 100644 --- a/fractional/generic_fractional/README.md +++ b/fractional/generic_fractional/README.md @@ -42,7 +42,7 @@ a regular FA2 fungible token, fractional owners can transfer it using standard FA2 transfer. **Vote on operation lambda** - any fractional owner can submit a vote consisting -of a lambda (`unit -> operation`) and a nonce. +of a lambda (`unit -> operation list`) and a nonce. The vote weight is proportional to a balance of the ownership token allocated to a fractional owner. Once predefined voting threshold is met, DAO executes the lambda and returns produced operation. @@ -61,7 +61,7 @@ lambda and returns produced operation. ```ocaml %vote { - lambda: unit -> operation; + lambda: unit -> operation list; nonce: nat; } ``` From 1f15c07499a2fce925d180c29f7574ef5f196c4f Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Tue, 16 Mar 2021 11:57:45 -0400 Subject: [PATCH 03/18] moved generic dao to top level --- .../generic_fractional => generic_fractional_dao}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {fractional/generic_fractional => generic_fractional_dao}/README.md (100%) diff --git a/fractional/generic_fractional/README.md b/generic_fractional_dao/README.md similarity index 100% rename from fractional/generic_fractional/README.md rename to generic_fractional_dao/README.md From 8e7eab72215be648ea3f4438f3588032e4db9757 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Tue, 16 Mar 2021 12:27:18 -0400 Subject: [PATCH 04/18] initial files for generic DAO --- .gitignore | 1 + fractional/README.md | 2 -- generic_fractional_dao/flextesa | 1 + generic_fractional_dao/ligo/fa2 | 1 + generic_fractional_dao/ligo/fa2_modules | 1 + generic_fractional_dao/ligo/src/fa2_single_token.mligo | 1 + 6 files changed, 5 insertions(+), 2 deletions(-) create mode 120000 generic_fractional_dao/flextesa create mode 120000 generic_fractional_dao/ligo/fa2 create mode 120000 generic_fractional_dao/ligo/fa2_modules create mode 120000 generic_fractional_dao/ligo/src/fa2_single_token.mligo diff --git a/.gitignore b/.gitignore index ac6649a..4da64f6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ node_modules **/*.pyc **/*.egg-info single_asset/lorentz/morley-ledgers/ +**/bisect*.coverage diff --git a/fractional/README.md b/fractional/README.md index d0f9ab9..6592082 100644 --- a/fractional/README.md +++ b/fractional/README.md @@ -27,8 +27,6 @@ Fraction of ownership is represented by the balance of the linked ownership toke allocated to the fractional owner. Fractional owner can vote to transfer an NFT to some other address. -**DAO admin** - a special privileged address which can setup initial fractional -NFT ownership and change some parameters of the ownership DAO. ## Operations diff --git a/generic_fractional_dao/flextesa b/generic_fractional_dao/flextesa new file mode 120000 index 0000000..e976725 --- /dev/null +++ b/generic_fractional_dao/flextesa @@ -0,0 +1 @@ +../shared/flextesa/ \ No newline at end of file diff --git a/generic_fractional_dao/ligo/fa2 b/generic_fractional_dao/ligo/fa2 new file mode 120000 index 0000000..6cb348d --- /dev/null +++ b/generic_fractional_dao/ligo/fa2 @@ -0,0 +1 @@ +../../shared/fa2 \ No newline at end of file diff --git a/generic_fractional_dao/ligo/fa2_modules b/generic_fractional_dao/ligo/fa2_modules new file mode 120000 index 0000000..147c835 --- /dev/null +++ b/generic_fractional_dao/ligo/fa2_modules @@ -0,0 +1 @@ +../../shared/fa2_modules/ \ No newline at end of file diff --git a/generic_fractional_dao/ligo/src/fa2_single_token.mligo b/generic_fractional_dao/ligo/src/fa2_single_token.mligo new file mode 120000 index 0000000..12ea74c --- /dev/null +++ b/generic_fractional_dao/ligo/src/fa2_single_token.mligo @@ -0,0 +1 @@ +../../../single_asset/ligo/src/fa2_single_token.mligo \ No newline at end of file From d7a4cd6c9614c7b747f9805569a54cab38276f02 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 10:31:37 -0400 Subject: [PATCH 05/18] WIP fractional dao --- .../ligo/src/fractional_dao.mligo | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 generic_fractional_dao/ligo/src/fractional_dao.mligo diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo new file mode 100644 index 0000000..3b1ff64 --- /dev/null +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -0,0 +1,71 @@ +#if !FRACTIONAL_DAO +#define FRACTIONAL_DAO + +#include "fa2_single_token.mligo" + +type permit = +{ + key : key; (* user's key *) + signature : signature; (*signature of packed vote_param + permit context *) +} + +type proposal_info = { + vote_amount : nat; + voters : address set; + timestamp : timestamp; + lambda: unit -> operation list +} + +type vote_param = +[@layout:comb] +{ + lambda : unit -> operation list; + permit : permit option; +} + +type set_voting_threshold_param = +(* [@layout: comb] *) +{ + old_threshold: nat; + new_threshold: nat; +} + +type dao_storage = { + ownership_token : single_token_storage; + voting_threshold : nat; + voting_period : nat; + vote_count : nat; + pending_proposals: (bytes, vote_param) big_map; +} + +type dao_entrypoints = + | Fa2 of fa2_entry_points + | Set_voting_threshold of set_voting_threshold_param + +[@inline] +let assert_self_call () = + if Tezos.sender = Tezos.self_address + then unit + else failwith "UNVOTED_CALL" + +let set_voting_threshold (t, s : set_voting_threshold_param * dao_storage) + : dao_storage = + if t.old_threshold <> s.voting_threshold + then (failwith "INVALID_OLD_THRESHOLD" : dao_storage) + else if t.new_threshold > s.ownership_token.total_supply + then (failwith "THRESHOLD_EXCEEDS_TOTAL_SUPPLY" : dao_storage) + else { s with voting_threshold = t.new_threshold; } + +let main(param, storage : dao_entrypoints * dao_storage) + : (operation list) * dao_storage = + match param with + | Fa2 fa2 -> + let ops, new_ownership = fa2_main(fa2, storage.ownership_token) in + ops, { storage with ownership_token = new_ownership; } + + | Set_voting_threshold t -> + let u = assert_self_call () in + let new_storage = set_voting_threshold (t, storage) in + ([] : operation list), new_storage + +#endif \ No newline at end of file From 95235c51f037ec30c655007a2d2dbc407b54c4ad Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 10:36:41 -0400 Subject: [PATCH 06/18] fractional dao voting entry point --- .../ligo/src/fractional_dao.mligo | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index 3b1ff64..3e32592 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -4,6 +4,7 @@ #include "fa2_single_token.mligo" type permit = +[@layout:comb] { key : key; (* user's key *) signature : signature; (*signature of packed vote_param + permit context *) @@ -16,15 +17,21 @@ type proposal_info = { lambda: unit -> operation list } -type vote_param = +type vote = [@layout:comb] { lambda : unit -> operation list; permit : permit option; } +type vote_param = +[@layout:comb] +{ + vote: vote; + permit : permit option; +} + type set_voting_threshold_param = -(* [@layout: comb] *) { old_threshold: nat; new_threshold: nat; @@ -35,12 +42,13 @@ type dao_storage = { voting_threshold : nat; voting_period : nat; vote_count : nat; - pending_proposals: (bytes, vote_param) big_map; + pending_proposals: (bytes, vote) big_map; } type dao_entrypoints = | Fa2 of fa2_entry_points | Set_voting_threshold of set_voting_threshold_param + | Vote of vote_param [@inline] let assert_self_call () = @@ -68,4 +76,7 @@ let main(param, storage : dao_entrypoints * dao_storage) let new_storage = set_voting_threshold (t, storage) in ([] : operation list), new_storage + | Vote v -> + ([] : operation list), storage + #endif \ No newline at end of file From 78debbbc1db36b618d451cabfbdc29616c3faf79 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 11:15:57 -0400 Subject: [PATCH 07/18] fractional dao WIP voting --- .../ligo/src/fractional_dao.mligo | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index 3e32592..3aeb4a9 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -7,14 +7,13 @@ type permit = [@layout:comb] { key : key; (* user's key *) - signature : signature; (*signature of packed vote_param + permit context *) + signature : signature; (*signature of packed lambda + permit context *) } type proposal_info = { vote_amount : nat; voters : address set; timestamp : timestamp; - lambda: unit -> operation list } type vote = @@ -24,13 +23,6 @@ type vote = permit : permit option; } -type vote_param = -[@layout:comb] -{ - vote: vote; - permit : permit option; -} - type set_voting_threshold_param = { old_threshold: nat; @@ -42,13 +34,13 @@ type dao_storage = { voting_threshold : nat; voting_period : nat; vote_count : nat; - pending_proposals: (bytes, vote) big_map; + pending_proposals: (bytes, proposal_info) big_map; } type dao_entrypoints = | Fa2 of fa2_entry_points | Set_voting_threshold of set_voting_threshold_param - | Vote of vote_param + | Vote of vote [@inline] let assert_self_call () = @@ -64,6 +56,37 @@ let set_voting_threshold (t, s : set_voting_threshold_param * dao_storage) then (failwith "THRESHOLD_EXCEEDS_TOTAL_SUPPLY" : dao_storage) else { s with voting_threshold = t.new_threshold; } +let check_vote (v, vote_count : vote * nat) : address = + match v.permit with + | None -> Tezos.sender + | Some permit -> + let signed_data = Bytes.pack ( + (Tezos.chain_id, Tezos.self_address), + (vote_count, v.lambda) + ) in + if true (* Crypto.check permit.key permit.signature signed_data *) + then Tezos.sender (* Tezos.address (Tezos.implicit_account (Crypto.hash_key (permit.key))) *) + else (failwith "MISSIGNED" : address) + +let add_vote (proposal, voter, ledger : proposal_info * address * ledger) + : proposal_info = + proposal + +let vote (v, s : vote * dao_storage) : (operation list) * dao_storage = + let voter = check_vote (v, s.vote_count) in + let vote_key = Bytes.pack v.lambda in + let pi : proposal_info option = Big_map.find_opt vote_key s.pending_proposals in + let proposal = match pi with + | Some pi -> pi + | None -> { + vote_amount = 0n; + voters = (Set.empty : address set); + timestamp = Tezos.now; + } + in + let update_proposal = add_vote (proposal, voter, s.ownership_token.ledger) in + ([] : operation list), s + let main(param, storage : dao_entrypoints * dao_storage) : (operation list) * dao_storage = match param with @@ -76,7 +99,6 @@ let main(param, storage : dao_entrypoints * dao_storage) let new_storage = set_voting_threshold (t, storage) in ([] : operation list), new_storage - | Vote v -> - ([] : operation list), storage + | Vote v -> vote (v, storage) #endif \ No newline at end of file From f1b7f86b3f5c0dc84b6a5cb9755b14f27e139f56 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 11:49:35 -0400 Subject: [PATCH 08/18] fractional dao voting WIP --- .../ligo/src/fractional_dao.mligo | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index 3aeb4a9..7393b1a 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -29,12 +29,14 @@ type set_voting_threshold_param = new_threshold: nat; } +type pending_proposals = (bytes, proposal_info) big_map + type dao_storage = { ownership_token : single_token_storage; voting_threshold : nat; voting_period : nat; vote_count : nat; - pending_proposals: (bytes, proposal_info) big_map; + pending_proposals: pending_proposals; } type dao_entrypoints = @@ -42,6 +44,8 @@ type dao_entrypoints = | Set_voting_threshold of set_voting_threshold_param | Vote of vote +type return = (operation list) * dao_storage + [@inline] let assert_self_call () = if Tezos.sender = Tezos.self_address @@ -56,39 +60,53 @@ let set_voting_threshold (t, s : set_voting_threshold_param * dao_storage) then (failwith "THRESHOLD_EXCEEDS_TOTAL_SUPPLY" : dao_storage) else { s with voting_threshold = t.new_threshold; } -let check_vote (v, vote_count : vote * nat) : address = - match v.permit with - | None -> Tezos.sender - | Some permit -> +let validate_permit (lambda, permit, vote_count + : (unit -> operation list) * permit * nat) : address = let signed_data = Bytes.pack ( (Tezos.chain_id, Tezos.self_address), - (vote_count, v.lambda) + (vote_count, lambda) ) in - if true (* Crypto.check permit.key permit.signature signed_data *) - then Tezos.sender (* Tezos.address (Tezos.implicit_account (Crypto.hash_key (permit.key))) *) + if Crypto.check permit.key permit.signature signed_data + then Tezos.address (Tezos.implicit_account (Crypto.hash_key (permit.key))) else (failwith "MISSIGNED" : address) +let is_expired (proposal, voting_period : proposal_info * nat) : bool = + if Tezos.now - proposal.timestamp > int(voting_period) + then true + else false + let add_vote (proposal, voter, ledger : proposal_info * address * ledger) : proposal_info = - proposal + if not Set.mem voter proposal.voters + then (failwith "NOT_A_VOTER" : proposal_info) + else proposal -let vote (v, s : vote * dao_storage) : (operation list) * dao_storage = - let voter = check_vote (v, s.vote_count) in - let vote_key = Bytes.pack v.lambda in - let pi : proposal_info option = Big_map.find_opt vote_key s.pending_proposals in - let proposal = match pi with +let get_proposal (key, pending_proposals : bytes * pending_proposals) : proposal_info = + let pi : proposal_info option = Big_map.find_opt key pending_proposals in + match pi with | Some pi -> pi | None -> { vote_amount = 0n; voters = (Set.empty : address set); timestamp = Tezos.now; } - in - let update_proposal = add_vote (proposal, voter, s.ownership_token.ledger) in - ([] : operation list), s -let main(param, storage : dao_entrypoints * dao_storage) - : (operation list) * dao_storage = +let vote (v, s : vote * dao_storage) : return = + let voter = match v.permit with + | None -> Tezos.sender + | Some p -> validate_permit (v.lambda, p, s.vote_count) + in + let vote_key = Bytes.pack v.lambda in + let proposal = get_proposal (vote_key, s.pending_proposals) in + if is_expired (proposal, s.voting_period) + then (failwith "EXPIRED" : return) + else if Set.mem voter proposal.voters + then (failwith "DUP_VOTE" : return) + else + let updated_proposal = add_vote (proposal, voter, s.ownership_token.ledger) in + ([] : operation list), s + +let main(param, storage : dao_entrypoints * dao_storage) : return = match param with | Fa2 fa2 -> let ops, new_ownership = fa2_main(fa2, storage.ownership_token) in From 0ffcdd45f65e637fc6f2513357a598ffe1ec59e6 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 12:13:58 -0400 Subject: [PATCH 09/18] fractional DAO vote WIP --- .../ligo/src/fractional_dao.mligo | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index 7393b1a..3f78534 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -75,36 +75,46 @@ let is_expired (proposal, voting_period : proposal_info * nat) : bool = then true else false -let add_vote (proposal, voter, ledger : proposal_info * address * ledger) - : proposal_info = - if not Set.mem voter proposal.voters - then (failwith "NOT_A_VOTER" : proposal_info) - else proposal - -let get_proposal (key, pending_proposals : bytes * pending_proposals) : proposal_info = - let pi : proposal_info option = Big_map.find_opt key pending_proposals in - match pi with - | Some pi -> pi - | None -> { - vote_amount = 0n; - voters = (Set.empty : address set); - timestamp = Tezos.now; - } +let get_voter_stake (voter, ledger : address * ledger) : nat = + match Big_map.find_opt voter ledger with + | None -> (failwith "NOT_VOTER" : nat) + | Some stake -> stake + +let update_proposal (proposal, vote_key, s : proposal_info * bytes * dao_storage) + : return = + ([] : operation list), s + +let execute_proposal (lambda, vote_key, s : (unit -> operation list) * bytes * dao_storage) + : return = + ([] : operation list), s let vote (v, s : vote * dao_storage) : return = let voter = match v.permit with | None -> Tezos.sender | Some p -> validate_permit (v.lambda, p, s.vote_count) in + let voter_stake = get_voter_stake (voter, s.ownership_token.ledger) in let vote_key = Bytes.pack v.lambda in - let proposal = get_proposal (vote_key, s.pending_proposals) in - if is_expired (proposal, s.voting_period) - then (failwith "EXPIRED" : return) - else if Set.mem voter proposal.voters - then (failwith "DUP_VOTE" : return) - else - let updated_proposal = add_vote (proposal, voter, s.ownership_token.ledger) in - ([] : operation list), s + let proposal = match Big_map.find_opt vote_key s.pending_proposals with + | None -> { + vote_amount = voter_stake; + voters = Set.literal [voter]; + timestamp = Tezos.now; + } + | Some p -> + if is_expired (p, s.voting_period) + then (failwith "EXPIRED" : proposal_info) + else if Set.mem voter p.voters + then (failwith "DUP_VOTE" : proposal_info) + else + { p with + vote_amount = p.vote_amount + voter_stake; + voters = Set.add voter p.voters; + } + in + if proposal.vote_amount < s.voting_threshold + then update_proposal (proposal, vote_key, s) + else execute_proposal (v.lambda, vote_key, s) let main(param, storage : dao_entrypoints * dao_storage) : return = match param with From afcafd85a176aced37de0632279c71abeb0f5ebc Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 12:16:24 -0400 Subject: [PATCH 10/18] fractional dao implemented vote --- generic_fractional_dao/ligo/src/fractional_dao.mligo | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index 3f78534..3aa255d 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -82,11 +82,14 @@ let get_voter_stake (voter, ledger : address * ledger) : nat = let update_proposal (proposal, vote_key, s : proposal_info * bytes * dao_storage) : return = - ([] : operation list), s + let new_pending = Big_map.update vote_key (Some proposal) s.pending_proposals in + ([] : operation list), { s with pending_proposals = new_pending; } let execute_proposal (lambda, vote_key, s : (unit -> operation list) * bytes * dao_storage) : return = - ([] : operation list), s + let new_pending = Big_map.remove vote_key s.pending_proposals in + let ops = lambda () in + ops, { s with pending_proposals = new_pending; } let vote (v, s : vote * dao_storage) : return = let voter = match v.permit with From cbd12f245bfb53b4c28cb2177eaae352247e2899 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 12:21:49 -0400 Subject: [PATCH 11/18] fractional dao set voting period --- .../ligo/src/fractional_dao.mligo | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index 3aa255d..6b2d698 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -29,6 +29,12 @@ type set_voting_threshold_param = new_threshold: nat; } +type set_voting_period_param = +{ + old_period: nat; + new_period: nat; +} + type pending_proposals = (bytes, proposal_info) big_map type dao_storage = { @@ -41,8 +47,11 @@ type dao_storage = { type dao_entrypoints = | Fa2 of fa2_entry_points - | Set_voting_threshold of set_voting_threshold_param | Vote of vote + (** self-governance entry point *) + | Set_voting_threshold of set_voting_threshold_param + (** self-governance entry point *) + | Set_voting_period of set_voting_period_param type return = (operation list) * dao_storage @@ -60,6 +69,12 @@ let set_voting_threshold (t, s : set_voting_threshold_param * dao_storage) then (failwith "THRESHOLD_EXCEEDS_TOTAL_SUPPLY" : dao_storage) else { s with voting_threshold = t.new_threshold; } +let set_voting_period (p, s : set_voting_period_param * dao_storage) + : dao_storage = + if p.old_period <> s.voting_period + then (failwith "INVALID_OLD_PERIOD" : dao_storage) + else { s with voting_period = p.new_period; } + let validate_permit (lambda, permit, vote_count : (unit -> operation list) * permit * nat) : address = let signed_data = Bytes.pack ( @@ -125,11 +140,16 @@ let main(param, storage : dao_entrypoints * dao_storage) : return = let ops, new_ownership = fa2_main(fa2, storage.ownership_token) in ops, { storage with ownership_token = new_ownership; } + | Vote v -> vote (v, storage) + | Set_voting_threshold t -> let u = assert_self_call () in let new_storage = set_voting_threshold (t, storage) in ([] : operation list), new_storage - | Vote v -> vote (v, storage) + | Set_voting_period p -> + let u = assert_self_call () in + let new_storage = set_voting_period (p, storage) in + ([] : operation list), new_storage #endif \ No newline at end of file From f34cefa5d60d71e5f977f0b8419fc30d2f6380e2 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 14:51:31 -0400 Subject: [PATCH 12/18] fractianal dao: flush expired --- .../ligo/src/fractional_dao.mligo | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index 6b2d698..68a66e0 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -16,10 +16,12 @@ type proposal_info = { timestamp : timestamp; } +type dao_lambda = unit -> operation list + type vote = [@layout:comb] { - lambda : unit -> operation list; + lambda : dao_lambda; permit : permit option; } @@ -43,6 +45,7 @@ type dao_storage = { voting_period : nat; vote_count : nat; pending_proposals: pending_proposals; + metadata : contract_metadata; } type dao_entrypoints = @@ -52,6 +55,8 @@ type dao_entrypoints = | Set_voting_threshold of set_voting_threshold_param (** self-governance entry point *) | Set_voting_period of set_voting_period_param + (** self-governance entry point *) + | Flush_expired of dao_lambda type return = (operation list) * dao_storage @@ -75,21 +80,33 @@ let set_voting_period (p, s : set_voting_period_param * dao_storage) then (failwith "INVALID_OLD_PERIOD" : dao_storage) else { s with voting_period = p.new_period; } -let validate_permit (lambda, permit, vote_count - : (unit -> operation list) * permit * nat) : address = - let signed_data = Bytes.pack ( - (Tezos.chain_id, Tezos.self_address), - (vote_count, lambda) - ) in - if Crypto.check permit.key permit.signature signed_data - then Tezos.address (Tezos.implicit_account (Crypto.hash_key (permit.key))) - else (failwith "MISSIGNED" : address) - let is_expired (proposal, voting_period : proposal_info * nat) : bool = if Tezos.now - proposal.timestamp > int(voting_period) then true else false +let flush_expired (lambda, s : dao_lambda * dao_storage ) : dao_storage = + let key = Bytes.pack lambda in + match Big_map.find_opt key s.pending_proposals with + | None -> (failwith "PROPOSAL_DOES_NOT_EXIST" : dao_storage) + | Some proposal -> + if is_expired(proposal, s.voting_period) + then + let new_pending = Big_map.remove key s.pending_proposals in + { s with pending_proposals = new_pending; } + else (failwith "PROPOSAL_NOT_EXPIRED" : dao_storage) + + +let validate_permit (lambda, permit, vote_count + : dao_lambda * permit * nat) : address = + let signed_data = Bytes.pack ( + (Tezos.chain_id, Tezos.self_address), + (vote_count, lambda) + ) in + if Crypto.check permit.key permit.signature signed_data + then Tezos.address (Tezos.implicit_account (Crypto.hash_key (permit.key))) + else (failwith "MISSIGNED" : address) + let get_voter_stake (voter, ledger : address * ledger) : nat = match Big_map.find_opt voter ledger with | None -> (failwith "NOT_VOTER" : nat) @@ -100,7 +117,7 @@ let update_proposal (proposal, vote_key, s : proposal_info * bytes * dao_storage let new_pending = Big_map.update vote_key (Some proposal) s.pending_proposals in ([] : operation list), { s with pending_proposals = new_pending; } -let execute_proposal (lambda, vote_key, s : (unit -> operation list) * bytes * dao_storage) +let execute_proposal (lambda, vote_key, s : dao_lambda * bytes * dao_storage) : return = let new_pending = Big_map.remove vote_key s.pending_proposals in let ops = lambda () in @@ -152,4 +169,9 @@ let main(param, storage : dao_entrypoints * dao_storage) : return = let new_storage = set_voting_period (p, storage) in ([] : operation list), new_storage -#endif \ No newline at end of file + | Flush_expired lambda -> + let u = assert_self_call () in + let new_storage = flush_expired (lambda, storage) in + ([] : operation list), new_storage + +#endif From 6ca1a433bb110f17902ad1b611e31169ab08e259 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 14:55:59 -0400 Subject: [PATCH 13/18] error codes --- generic_fractional_dao/ligo/src/fractional_dao.mligo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index 68a66e0..de9a39d 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -94,7 +94,7 @@ let flush_expired (lambda, s : dao_lambda * dao_storage ) : dao_storage = then let new_pending = Big_map.remove key s.pending_proposals in { s with pending_proposals = new_pending; } - else (failwith "PROPOSAL_NOT_EXPIRED" : dao_storage) + else (failwith "NOT_EXPIRED" : dao_storage) let validate_permit (lambda, permit, vote_count From f8aec61e478ea438f72838eaca1cd061cac75c5c Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Fri, 19 Mar 2021 18:26:14 -0400 Subject: [PATCH 14/18] added some files for generic DAO tests --- generic_fractional_dao/tests/.prettierrc | 9 +++ generic_fractional_dao/tests/jest.config.js | 5 ++ generic_fractional_dao/tests/ligo.ts | 1 + generic_fractional_dao/tests/tsconfig.json | 69 +++++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 generic_fractional_dao/tests/.prettierrc create mode 100644 generic_fractional_dao/tests/jest.config.js create mode 120000 generic_fractional_dao/tests/ligo.ts create mode 100644 generic_fractional_dao/tests/tsconfig.json diff --git a/generic_fractional_dao/tests/.prettierrc b/generic_fractional_dao/tests/.prettierrc new file mode 100644 index 0000000..e59c880 --- /dev/null +++ b/generic_fractional_dao/tests/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "tabWidth": 2, + "trailingComma": "none", + "singleQuote": true, + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 80 +} diff --git a/generic_fractional_dao/tests/jest.config.js b/generic_fractional_dao/tests/jest.config.js new file mode 100644 index 0000000..5699881 --- /dev/null +++ b/generic_fractional_dao/tests/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node' + // testMatch: ['**/__tests__/*.+(spec|test).[jt]s?(x)'] +}; diff --git a/generic_fractional_dao/tests/ligo.ts b/generic_fractional_dao/tests/ligo.ts new file mode 120000 index 0000000..a2b425a --- /dev/null +++ b/generic_fractional_dao/tests/ligo.ts @@ -0,0 +1 @@ +../../shared/typescript/ligo.ts \ No newline at end of file diff --git a/generic_fractional_dao/tests/tsconfig.json b/generic_fractional_dao/tests/tsconfig.json new file mode 100644 index 0000000..6f7e497 --- /dev/null +++ b/generic_fractional_dao/tests/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} From def476bf45a6e6ec686e4eda294b30f895d3e1f6 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Mon, 22 Mar 2021 12:02:19 -0400 Subject: [PATCH 15/18] fractional sample storage --- .../ligo/src/fractional_dao.mligo | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index de9a39d..a4f5417 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -174,4 +174,41 @@ let main(param, storage : dao_entrypoints * dao_storage) : return = let new_storage = flush_expired (lambda, storage) in ([] : operation list), new_storage + +(* let token : single_token_storage = { + +} *) + +let sample_storage : dao_storage = { + ownership_token = { + ledger = Big_map.literal [ + (("tz1YPSCGWXwBdTncK2aCctSZAXWvGsGwVJqU" : address), 50n); + (("KT193LPqieuBfx1hqzXGZhuX2upkkKgfNY9w" : address), 50n); + ]; + operators = (Big_map.empty : operator_storage); + token_metadata = Big_map.literal [ + ( 0n, + { + token_id = 0n; + token_info = Map.literal [ + ("symbol", 0x544b31); + ("name", 0x5465737420546f6b656e); + ("decimals", 0x30); + ]; + } + ); + ]; + total_supply = 100n; + }; + voting_threshold = 75n; + voting_period = 10000000n; + vote_count = 0n; + pending_proposals = (Big_map.empty : pending_proposals); + metadata = Big_map.literal [ + ("", Bytes.pack "tezos-storage:content" ); + ("", 0x00); + ("content", 0x00) (* bytes encoded UTF-8 JSON *) + ]; +} + #endif From e11894139b10a123cef4fa22191c96515669ade5 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Mon, 22 Mar 2021 12:06:37 -0400 Subject: [PATCH 16/18] flush_expired entry point is unguarded --- generic_fractional_dao/ligo/src/fractional_dao.mligo | 1 - 1 file changed, 1 deletion(-) diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index a4f5417..a6de266 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -170,7 +170,6 @@ let main(param, storage : dao_entrypoints * dao_storage) : return = ([] : operation list), new_storage | Flush_expired lambda -> - let u = assert_self_call () in let new_storage = flush_expired (lambda, storage) in ([] : operation list), new_storage From 906d988d87b1836c9537b2590aafd6055ab3d8e4 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Mon, 22 Mar 2021 12:14:33 -0400 Subject: [PATCH 17/18] guard minimum voting period --- generic_fractional_dao/README.md | 11 +++++++++++ generic_fractional_dao/ligo/src/fractional_dao.mligo | 2 ++ 2 files changed, 13 insertions(+) diff --git a/generic_fractional_dao/README.md b/generic_fractional_dao/README.md index 62f24bf..bca5594 100644 --- a/generic_fractional_dao/README.md +++ b/generic_fractional_dao/README.md @@ -74,3 +74,14 @@ lambda functions for generic operations should be considered. 1. NFT `transfer` and `update_operators`. 2. Interaction with market place and auction contracts. 3. DAO administration. + +## What's Next + +- Create tests for the fractional DAO contract +- Create helper Typescript API to generate DAO lambdas for the most common operations + + - Transfer governed token(s) + - Update operators for governed token(s) + - Change DAO voting threshold + - Change DAO voting period + - Migrate DAO contract to minter-sdk diff --git a/generic_fractional_dao/ligo/src/fractional_dao.mligo b/generic_fractional_dao/ligo/src/fractional_dao.mligo index a6de266..9bd7655 100644 --- a/generic_fractional_dao/ligo/src/fractional_dao.mligo +++ b/generic_fractional_dao/ligo/src/fractional_dao.mligo @@ -78,6 +78,8 @@ let set_voting_period (p, s : set_voting_period_param * dao_storage) : dao_storage = if p.old_period <> s.voting_period then (failwith "INVALID_OLD_PERIOD" : dao_storage) + else if p.new_period < 300n + then (failwith "PERIOD_TOO_SHORT" : dao_storage) else { s with voting_period = p.new_period; } let is_expired (proposal, voting_period : proposal_info * nat) : bool = From cb7d70255fdcdd4b4ba0be863d48e417280c0405 Mon Sep 17 00:00:00 2001 From: Eugene Mishura Date: Mon, 22 Mar 2021 12:15:00 -0400 Subject: [PATCH 18/18] edited readme --- generic_fractional_dao/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generic_fractional_dao/README.md b/generic_fractional_dao/README.md index bca5594..e1f940f 100644 --- a/generic_fractional_dao/README.md +++ b/generic_fractional_dao/README.md @@ -84,4 +84,5 @@ lambda functions for generic operations should be considered. - Update operators for governed token(s) - Change DAO voting threshold - Change DAO voting period - - Migrate DAO contract to minter-sdk + +- Migrate DAO contract to minter-sdk