Skip to content
This repository has been archived by the owner on Feb 15, 2024. It is now read-only.

Draft of generic fractional ownership DAO #57

Merged
merged 18 commits into from
Mar 22, 2021
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ node_modules
**/*.pyc
**/*.egg-info
single_asset/lorentz/morley-ledgers/
**/bisect*.coverage
2 changes: 0 additions & 2 deletions fractional/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
88 changes: 88 additions & 0 deletions generic_fractional_dao/README.md
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
1 change: 1 addition & 0 deletions generic_fractional_dao/flextesa
1 change: 1 addition & 0 deletions generic_fractional_dao/ligo/fa2
1 change: 1 addition & 0 deletions generic_fractional_dao/ligo/fa2_modules
1 change: 1 addition & 0 deletions generic_fractional_dao/ligo/src/fa2_single_token.mligo
215 changes: 215 additions & 0 deletions generic_fractional_dao/ligo/src/fractional_dao.mligo
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)
Copy link

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

) 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
9 changes: 9 additions & 0 deletions generic_fractional_dao/tests/.prettierrc
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
}
5 changes: 5 additions & 0 deletions generic_fractional_dao/tests/jest.config.js
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)']
};
1 change: 1 addition & 0 deletions generic_fractional_dao/tests/ligo.ts
Loading