This repository has been archived by the owner on Feb 15, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 31
Draft of generic fractional ownership DAO #57
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
0ec27f4
draft of generic fractional ownership DAO
93bf299
operation lambda can return a list of operations
1f15c07
moved generic dao to top level
8e7eab7
initial files for generic DAO
d7a4cd6
WIP fractional dao
95235c5
fractional dao voting entry point
78debbb
fractional dao WIP voting
f1b7f86
fractional dao voting WIP
0ffcdd4
fractional DAO vote WIP
afcafd8
fractional dao implemented vote
cbd12f2
fractional dao set voting period
f34cefa
fractianal dao: flush expired
6ca1a43
error codes
f8aec61
added some files for generic DAO tests
def476b
fractional sample storage
e118941
flush_expired entry point is unguarded
906d988
guard minimum voting period
cb7d702
edited readme
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,4 @@ node_modules | |
**/*.pyc | ||
**/*.egg-info | ||
single_asset/lorentz/morley-ledgers/ | ||
**/bisect*.coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# 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 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. | ||
|
||
## DAO Entry Points | ||
|
||
### Standard FA2 entry points for ownership token | ||
|
||
`%transfer` | ||
|
||
`balance_of` | ||
|
||
`update_operators` | ||
|
||
### `%vote` | ||
|
||
```ocaml | ||
%vote { | ||
lambda: unit -> operation list; | ||
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. | ||
|
||
## 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../shared/flextesa/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../shared/fa2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../shared/fa2_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../single_asset/ligo/src/fa2_single_token.mligo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
#if !FRACTIONAL_DAO | ||
#define FRACTIONAL_DAO | ||
|
||
#include "fa2_single_token.mligo" | ||
|
||
type permit = | ||
[@layout:comb] | ||
{ | ||
key : key; (* user's key *) | ||
signature : signature; (*signature of packed lambda + permit context *) | ||
} | ||
|
||
type proposal_info = { | ||
vote_amount : nat; | ||
voters : address set; | ||
timestamp : timestamp; | ||
} | ||
|
||
type dao_lambda = unit -> operation list | ||
|
||
type vote = | ||
[@layout:comb] | ||
{ | ||
lambda : dao_lambda; | ||
permit : permit option; | ||
} | ||
|
||
type set_voting_threshold_param = | ||
{ | ||
old_threshold: nat; | ||
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 = { | ||
ownership_token : single_token_storage; | ||
voting_threshold : nat; | ||
voting_period : nat; | ||
vote_count : nat; | ||
pending_proposals: pending_proposals; | ||
metadata : contract_metadata; | ||
} | ||
|
||
type dao_entrypoints = | ||
| Fa2 of fa2_entry_points | ||
| 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 | ||
(** self-governance entry point *) | ||
| Flush_expired of dao_lambda | ||
|
||
type return = (operation list) * dao_storage | ||
|
||
[@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 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 = | ||
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 "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) | ||
| Some stake -> stake | ||
|
||
let update_proposal (proposal, vote_key, s : proposal_info * bytes * dao_storage) | ||
: return = | ||
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 : dao_lambda * bytes * dao_storage) | ||
: return = | ||
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 | ||
| 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 = 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 | ||
| Fa2 fa2 -> | ||
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 | ||
|
||
| Set_voting_period p -> | ||
let u = assert_self_call () in | ||
let new_storage = set_voting_period (p, storage) in | ||
([] : operation list), new_storage | ||
|
||
| Flush_expired lambda -> | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"semi": true, | ||
"tabWidth": 2, | ||
"trailingComma": "none", | ||
"singleQuote": true, | ||
"bracketSpacing": true, | ||
"arrowParens": "avoid", | ||
"printWidth": 80 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node' | ||
// testMatch: ['**/__tests__/*.+(spec|test).[jt]s?(x)'] | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../shared/typescript/ligo.ts |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't follow exactly the signing procedure in Tzip-17(https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-17/tzip-17.md#submission) where the parameter is packed and then hashed with
BLAKE2B
but I suppose that is unnecessary in this context since the un-obfuscated parameter is passed to the entrypoint at the same time anyway