diff --git a/contracts/guild-factory/README.md b/contracts/guild-factory/README.md index e69de29..68fd8cc 100644 --- a/contracts/guild-factory/README.md +++ b/contracts/guild-factory/README.md @@ -0,0 +1,81 @@ +# Guild Factory + +## Objective +- Easily allow someone to create a guild +- Make composable for a guild of any type +- Allow a member to level up, this should also decay without participation +- Allow path's or journeys that lead to apprenticeships +- Make members accountable to each other via a reputation system or vouch +- Always be optimistic by preventing fraud before it occurs +- Funding payment system should be seperate + + +## Inspiration +- [Raid Guild](https://handbook.raidguild.org/) +- [Near Guilds](https://near.org/start-a-guild/) +- [Guild.xyz](https://guild.xyz/) + +## Guild + +Guilds can be created by anyone, they are intentionally bare to allow composablity for any use case + +Screen Shot 2022-08-05 at 12 38 31 PM + +## Constitution + +A `Guild` has a `Constitution`. These are a set of rules that `Member`s should abide by. + +## Member + +Once a user joins a guild, the `Member` resource is added to the users account. It will track the `Guild`s that a `Member` is associated with. + +## Apprenticeship + +A guild has `Apprenticeships`. These are badges/credentials([Soul Based Tokens?](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4105763)) that a `Member` earns if they complete a series of specified `Journeys`. + +### Journeys + +`Journey`s are pathways that consist of a series of `Task`s. These are meant to capture a certain skill set eg Error Handling in Rust. + +### Tasks + +`Task`s are a step in gaining a skill set(`Journey`). These can be anything and only have two prerequsites(name and description). They track users submissions by a `Member` submitting a deliverable. + +## Reputation + +> WIP + +three varibles in place that could be used to determine reputation(just to get the conversation flowing): + +1. Vouching by other users +2. Member Score: Completion rate at different levels eg Apprenticeship and Journey +3. Deliverable scoring for tasks by peers(anonymous) + +## Payments + +> WIP + + +## Basic Functionality + +**Guild functions** + +- Create Guild +- Create Apprenticeship +- Add Constitution +- Add/remove Constitution rule +- Create Membership +- Add/remove Role + + +**Apprenticeship functions** + +- Add Journey +- Add Task +- Add Deliverable + + +**Member Fuctions** + +- Add/remove Detail +- Change Detail \ No newline at end of file diff --git a/contracts/guild-factory/sources/GuildFactory.move b/contracts/guild-factory/sources/GuildFactory.move index e69de29..ae05205 100644 --- a/contracts/guild-factory/sources/GuildFactory.move +++ b/contracts/guild-factory/sources/GuildFactory.move @@ -0,0 +1,544 @@ +// SPDX-License-Identifier: MIT + +/// @title GuildFactory +/// @notice Create composable guilds for public use. +/// @dev This is a factory for creating `Guild`s. + + +address Std { + module GuildFactory { + use Std::GAS::GAS; + use Std::Vector; + use Std::Signer; + use Std::DiemConfig; + use Std::Diem; + use Std::DiemAccount; + + const ADDRESS_DOES_NOT_CONTAIN_GUILD = 1; + const GUILD_DOES_NOT_CONTAIN_JOURNEY = 2; + const INDEX_DOES_NOT_EXIST = 3; + const CANNOT_REMOVE_DEFAULT_ROLE = 4; + const GUILD_DOES_NOT_CONTAIN_APPRENTICESHIP = 5; + const USER_DOES_NOT_HAVE_APPRENTICESHIP = 6; + const GUILD_APPRENTICESHIP_DOES_NOT_CONTAIN_MEMBER_SCORE = 7; + const GUILD_JOURNEY_DOES_NOT_CONTAIN_MEMBER_SCORE = 8; + const GUILD_DOES_NOT_CONTAIN_TASK = 9; + + // @dev The member resource that is store on a users account to track their memberships. + struct Member has key, store { + memberships: Vector
, + details: vector, + } + + // @dev The membership resource that is stored on a member and guild account to track. + struct Membership has key, copy { + guild: address, + user: address, + role: Role, + level: u64, + reputation: Reputation, + } + + // @dev allows members to specify outside credentials that may be applicable for indentity verification. + struct Detail { + key: vector, + value: vector + } + + + // @dev guilds are groups of members that are trying to achieve a particular goal/s. + struct Guild has key, store { + name: string, + description: string, + constitution: vector>, + members: Vector, + roles: Vector, + apprenticeships: Vector, + details: Vector, + created: u64, + updated: u64, + active: bool, + } + + // @dev A role that a guild is using and the reputation score. + struct Role has copy { + name: vector, + reputation: u64, + } + + + //TODO: use Memberscore and vouching to determine reputation weight and role. + // @dev reputation keeps track of the credentials a member earns in a guild and if their peers support them. + struct Reputation { + vouchers: vector
, + credentials: vector, + weight: u64, + + } + + //TODO: use Member score as a mechanism to determine reputation. + // @dev keeps track of the score a member earns in a apprenticeship/journey compared to the completed state. + struct MemberScore { + user: address, + score: u64, + } + + // @dev apprenticeships is a badge of honor that a member would get after completing a set of journeys. + struct Apprenticeship { + uid: u64, + name: string, + description: string, + journeys: Vector, + member_scores: Vector, + created: u64, + updated: u64, + active: bool, + } + + // @dev journeys consist of a group of tasks that would help them learn a particular skill or complete a mission. + struct Journey { + name: string, + description: string, + tasks: Vector, + member_scores: Vector, + created: u64, + updated: u64, + active: bool, + } + + // @dev tasks are individual components within a journey that have a specific deliverable. + struct Task { + name: vector, + description: vector, + deliverable: vector, + created: u64, + active: bool, + } + + // @dev deliverables are for members to post submissions to tasks. + struct Deliverable { + member: address, + link: vector, + } + + + + ///////// INIT FUNCTIONS //////// + + // @dev create guild, two values must be specified. + public entry fun init_guild(sender: &signer, name: vector, description: vector ) { + //Role is defaulted for any new member + let r = Role( + name: 'New Member', + reputation: 0); + + let g = Guild( + name: name, + description: description, + constitution: Vector::empty>(), + members: Vector::empty(), + roles: Vector::empty(), + apprenticeships: Vector::empty(), + details: Vector::empty(), + created: DiemConfig::get_current_epoch(), + updated: DiemConfig::get_current_epoch(), + active: true, + ); + + Vector::push_back(g.roles,r); + + move_to(_sender, g) + } + + // @dev create Member resource on the user account if the user does not already have it. + fun init_member(_sender: &signer) { + if (!exists(Signer::address_of(_sender))) { + let new_details = init_details(); + move_to(_sender, Member { + memberships: Vector::empty
(), + details: new_details, + }) + } + } + + + + ///////// CREATE FUNCTIONS //////// + + /// @dev create a new membership for the guild. + fun create_membership(_sender: &signer, guild: address) { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + + //create member resource on sender if they do not currently have it. + init_member(_sender); + let g = borrow_global(guild) + let u = borrow_global(Signer::address_of(_sender)) + let r = Reputation( + vouchers: Vector::create
(), + credentials: Vector::create(), + weight: 0, + ); + + let m = Membership( + guild: guild, + user: Signer::address_of(_sender), + role: Role( + name: 'New Member', + reputation: 0, + ), + level: Vector::empty(), + reputation: r, + ); + Vector::push_back
(&u.memberships, guild); + Vector::push_back(&g.members, m); + + } + + // @dev create a new journey for a guild. + fun create_journey(guild: address, apprenticeship: u64, name: vector, description: vector) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + + let g = borrow_global(guild) + let j = Journey( + name: name, + description: description, + tasks: Vector::empty(), + member_scores: Vector::empty(), + created: DiemConfig::get_current_epoch(), + updated: DiemConfig::get_current_epoch(), + active: true, + ); + Vector::push_back(&g.journeys, j); + } + + fun create_task(guild: address, journey: vector, name: vector, description: vector) { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + + + let g = borrow_global(guild) + let (t, i) = get_Journey_index_by_name(guild, name); + assert(!t, GUILD_DOES_NOT_CONTAIN_JOURNEY); + + let j = Vector::borrow(&g.journeys, i); + + let t = Task( + name: name, + description: description, + deliverable: Vector::empty, + created: DiemConfig::get_current_epoch(), + active: true, + ); + Vector::push_back(&j.tasks, t); + } + + + ///////// GUILD UTILS //////// + + fun create_constitution(guild: address, constitution: vector>) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let g = borrow_global(guild) + g.constitution = constitution; + } + + + fun add_constitution_rule(guild: address, rule: vector) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let g = borrow_global(guild) + Vector::push_back>(&g.constitution, rule); + } + + + fun remove_constitution_rule(guild: address, index: u64) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let len = Vector::length(&g.constitution); + let g = borrow_global_mut(guild); + assert(index <= len - 1 , INDEX_DOES_NOT_EXIST); + Vector::remove(&mut g.constitution, index); + } + + //TODO: check reputation is below threshold + fun add_role(guild: address, name: vector, reputation: u64) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let g = borrow_global_mut(guild) + let r = Role( + name: name, + reputation: reputation, + ); + Vector::push_back
(&mut g.roles, r); + } + + + //TODO: check if members have role, no members should have a role that is being removed. + fun remove_role(guild: address, name: vector) acquires Guild { + assert(name == 'New Member', CANNOT_REMOVE_DEFAULT_ROLE); + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let len = Vector::length(&g.roles); + let g = borrow_global_mut(guild); + assert(index <= len - 1 , INDEX_DOES_NOT_EXIST); + Vector::remove(&mut g.roles, index); + } + + + + ///////// COMPLETION UTILS //////// + + //TODO: likely a better way to bubble up the completion. Seems super inefficient. + //@dev update the completion(MemberScore) of a Apprenticship for a user. + fun update_apprenticeship_completion(user: address, guild: address, name: vector) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let g = borrow_global(guild); + let (t, i) = get_apprenticeship_index_by_name(guild, name); + if(!t) { + assert(false, GUILD_DOES_NOT_CONTAIN_APPRENTICESHIP); + } + let a = Vector::borrow(&g.apprenticeships, i); + let journey_length = Vector::length(&a.journeys); + let apprenticeship_score = 0; + let i = 0; + while(i < journey_length){ + let j = 0; + let journey = Vector::borrow(&a.journeys, i); + let ms_len = Vector::length(&journey.member_scores); + while(j < ms_len){ + let ms = Vector::borrow(&journey.member_scores, j); + if(ms.user == user){ + apprenticeship_score += ms.score; + } + } + } + let (t2, i2) = get_apprenticeship_member_score_index(a, user); + if(!t2) { + assert(false, GUILD_APPRENTICESHIP_DOES_NOT_CONTAIN_MEMBER_SCORE); + } + let ams = Vector::borrow(&a.member_scores, i2); + ams.score = apprenticeship_score; + } + + //@dev update the completion(MemberScore) of a Journey for a user. + fun update_journey_completion(user: address, guild: address, name: vector) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let g = borrow_global(guild); + let (t, i) = get_journey_index_by_name(guild, name); + if(!t) { + assert(false, GUILD_DOES_NOT_CONTAIN_JOURNEY); + } + let j = Vector::borrow(&g.journeys, i); + let journey_length = Vector::length(&j.tasks); + let journey_score = 0; + let i = 0; + while(i < journey_length){ + let t = Vector::borrow(&j.tasks, i); + let d_len = Vector::length(&t.deliverable); + let j = 0; + while(j < d_len){ + let d = Vector::borrow(&t.deliverable, j); + if(d.user == user){ + journey_score += d.score; + } + } + } + let (t2, i2) = get_journey_member_score_index(j, user); + if(!t2) { + assert(false, GUILD_JOURNEY_DOES_NOT_CONTAIN_MEMBER_SCORE); + } + let jms = Vector::borrow(&j.member_scores, i2); + jms.score = journey_score; + } + + //@dev Get the completion status(MemberScore) of an apprenticeship for a user. + fun get_apprenticeship_percentage_completion(user: address, guild: address, name: vector) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let g = borrow_global(guild); + let (t, i) = get_apprenticeship_index_by_name(guild, name); + if(!t) { + assert(false, GUILD_DOES_NOT_CONTAIN_APPRENTICESHIP); + } + let a = Vector::borrow(&g.apprenticeships, i); + let ms_len = Vector::length(&a.member_scores); + let i = 0; + while(i < ms_len){ + let ms = Vector::borrow(&a.member_scores, i); + + if (ms.address == user) return (true, ms.score); + + i = i + 1; + }; + (false, 0) + } + + + ///////// TASK INTERATIONS //////// + + fun add_deliverable(guild: address, task: vector, apprenticeship: vector, journey: vector) acquires Guild { + assert(!exists(address), ADDRESS_DOES_NOT_CONTAIN_GUILD); + let g = borrow_global_mut(guild); + let d = Deliverable( + name: name, + score: 0, + ); + let (t, i) = get_apprenticeship_index_by_name(guild, apprenticeship); + if(!t) { + assert(false, GUILD_DOES_NOT_CONTAIN_APPRENTICESHIP); + } + let a = Vector::borrow_mut(&mut g.apprenticeships, i); + let (t2, i2) = get_journey_index_by_name(guild, journey); + if(!t2) { + assert(false, GUILD_DOES_NOT_CONTAIN_JOURNEY); + } + let j = Vector::borrow_mut(&mut a.journeys, i2); + let (t3, i3) = get_task_index_by_name(guild, task); + if(!t3) { + assert(false, GUILD_DOES_NOT_CONTAIN_TASK); + } + let t = Vector::borrow_mut(&mut j.tasks, i3); + Vector::push(&mut t.deliverable, d); + + //bubble up completion to update scores. + update_journey_completion(g.owner, guild, journey); + update_apprenticeship_completion(g.owner, guild, apprenticeship); + + } + + + + ///////// UTILS //////// + + fun init_details(): Vector acquires Detail{ + //creates a details array for a worker + let details = Vector::empty(); + //initialized with some defaults Alias, github, twitter, discord + Vector::push_back(details,Detail {key: 'alias',value: Vector::empty()}); + Vector::push_back(details,Detail {key: 'github',value: Vector::empty()}); + Vector::push_back(details,Detail {key: 'twitter',value: Vector::empty()}); + Vector::push_back(details,Detail {key: 'discord',value: Vector::empty()}); + + details + } + + + //TODO: make below three details functions reusable between guild and Member + public fun add_detail(_sender: &signer, key: vector, value: vector) acquires Member, Details { + let w = borrow_global_mut(_sender); + Vector::push_back(&w.details, Detail{ + key: key, + value: value + }) + } + + + public fun remove_detail(_sender: &signer, key: vector) acquires Member, Details { + let m = borrow_global_mut(_sender); + let (t,i) = get_index_by_key(Signer::address_of(_sender)); + assert(!t, KEY_DOES_NOT_EXIST); + if (t) { + Vector::remove(&mut m.details, i); + } + } + + + public fun change_detail(_sender: &signer, key: vector, value: vector) acquires Member, Details { + let m = borrow_global_mut(_sender); + let (t,i) = get_index_by_key(Signer::address_of(_sender)); + assert(!t, KEY_DOES_NOT_EXIST); + if (t) { + Vector::remove(&mut m.details, i); + let d = Vector::borrow(&mut m.details, i); + d.value = value; + } + + } + + + fun get_index_by_key(_worker: address, _key: Vector): (bool, u64) acquires Worker, Detail { + let w = borrow_global(_worker); + let len = Vector::length(&w.details); + + let i = 0; + while (i < len) { + let d = Vector::borrow(&w.details, i); + + if (d.key == _key) return (true, i); + + i = i + 1; + }; + (false, 0) + } + + + // @dev removes an element from the list of payments, and returns in to scope. + fun get_journey_index_by_name(_guild_addr: address, _name: vector): (bool, u64) acquires Guild { + let g = borrow_global(_guild_addr); + + let len = Vector::length(&g.journeys); + + let i = 0; + while (i < len) { + let j = Vector::borrow(&g.journeys, i); + + if (g.name == _name) return (true, i); + + i = i + 1; + }; + (false, 0) + } + + + // @dev removes an element from the list of payments, and returns in to scope. + fun get_role_index_by_name(_guild_addr: address, _name: vector): (bool, u64) acquires Guild { + let g = borrow_global(_guild_addr); + + let len = Vector::length(&g.roles); + + let i = 0; + while (i < len) { + let j = Vector::borrow(&g.roles, i); + + if (j.name == _name) return (true, i); + + i = i + 1; + }; + (false, 0) + } + + + fun get_apprenticeship_index_by_name(_guild_addr: address, _name: vector): (bool, u64) acquires Guild { + let g = borrow_global(_guild_addr); + + let len = Vector::length(&g.apprenticeships); + + let i = 0; + while (i < len) { + let a = Vector::borrow(&g.apprenticeship, i); + + if (a.name == _name) return (true, i); + + i = i + 1; + }; + (false, 0) + } + + + fun get_apprenticeship_member_score_index(&mut _apprenticeship: Apprenticeship, user: address): (bool, u64) acquires Apprenticeship { + let ms_len = Vector::length(&_apprenticeship.member_scores); + let i = 0; + while (i < ms_len) { + let ms = Vector::borrow(&_apprenticeship.member_scores, i); + if (ms.address == user) return (true,i); + i = i + 1; + }; + (false,0) + } + + + fun get_journey_member_score_index(&mut _journey: Journey, user: address): (bool, u64) acquires Journey { + let ms_len = Vector::length(&_journey.member_scores); + let i = 0; + while (i < ms_len) { + let ms = Vector::borrow(&_journey.member_scores, i); + if (ms.address == user) return (true,i); + i = i + 1; + }; + (false,0) + } +} +} \ No newline at end of file