diff --git a/package/Move.toml b/package/Move.toml index 0dccfc7..cbc0ab0 100644 --- a/package/Move.toml +++ b/package/Move.toml @@ -3,19 +3,20 @@ name = "Kraken" edition = "2024.beta" license = "MIT" authors = ["Thouny (thouny@tuta.io)", "Jose (jose@interestprotocol.com)"] -published-at = "0x2eac5dd46537bf961f931f958a104a99ec6328f0de646ad2d08127fd0b31566e" [dependencies] -Sui = { override = true, git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "testnet" } -Kiosk = { git = "https://github.com/MystenLabs/apps.git", subdir = "kiosk", rev = "testnet" } +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } +Kiosk = { git = "https://github.com/MystenLabs/apps.git", subdir = "kiosk", rev = "main" } # [dev-dependencies] # Kiosk = { git = "https://github.com/MystenLabs/apps.git", subdir = "kiosk", rev = "main" } # Kiosk = { local = "../../kraken-sdk/test/packages/kiosk" } # for tests on localnet [addresses] -kraken = "0x2eac5dd46537bf961f931f958a104a99ec6328f0de646ad2d08127fd0b31566e" -kiosk = "0xbd8fc1947cf119350184107a3087e2dc27efefa0dd82e25a1f699069fe81a585" +kraken = "0x0" +std = "0x1" +sui = "0x2" +kiosk = "0x434b5bd8f6a7b05fede0ff46c6e511d71ea326ed38056e3bcd681d2d7c2a7879" # [dev-addresses] # kiosk = "0x434b5bd8f6a7b05fede0ff46c6e511d71ea326ed38056e3bcd681d2d7c2a7879" \ No newline at end of file diff --git a/package/sources/actions/config.move b/package/sources/actions/config.move index b39c86b..7cf695c 100644 --- a/package/sources/actions/config.move +++ b/package/sources/actions/config.move @@ -5,7 +5,8 @@ /// Teams can choose to use any version of the package and must explicitly migrate to the new version. module kraken::config { - use std::string::String; + use std::string::{Self, String}; + use sui::vec_map::{Self, VecMap}; use kraken::multisig::{Multisig, Executable, Proposal}; // === Errors === @@ -14,28 +15,51 @@ module kraken::config { const ENotMember: u64 = 1; const EAlreadyMember: u64 = 2; const EThresholdNull: u64 = 3; - const EModifyNotExecuted: u64 = 4; - const EMigrateNotExecuted: u64 = 5; - const EVersionAlreadyUpdated: u64 = 6; + const EMigrateNotExecuted: u64 = 4; + const EVersionAlreadyUpdated: u64 = 5; + const ENameAlreadySet: u64 = 6; + const ENameNotSet: u64 = 7; + const EMembersNotExecuted: u64 = 8; + const EWeightsNotExecuted: u64 = 9; + const ERolesNotExecuted: u64 = 10; // === Structs === - public struct Witness has drop {} + // delegated witness verifying a proposal is destroyed in the module where it was created + public struct Witness has copy, drop {} - // [ACTION] upgrade is separate for better ux - public struct Modify has store { - // new name if any - name: Option, - // new threshold, has to be <= to new total addresses - threshold: Option, - // addresses to remove, executed before adding new members + // [ACTION] change the name + public struct Name has store { + // new name + name: String, + } + + // [ACTION] add or remove members + public struct Members has store { + // addresses to remove to_remove: vector
, // addresses to add to_add: vector
, - // weights of the members that will be added + } + + // [ACTION] modify weights and threshold (total weigth) + public struct Weights has store { + // new threshold, has to be <= to new total weight + threshold: Option, + // addresses to modify + addresses: vector
, + // new weights of the members weights: vector, } + // [ACTION] add or remove roles from chosen members + public struct Roles has store { + // roles to add to each address + to_add: VecMap>, + // roles to remove from each address + to_remove: VecMap>, + } + // [ACTION] update the version of the multisig public struct Migrate has store { // the new version @@ -44,22 +68,57 @@ module kraken::config { // === [PROPOSAL] Public functions === - // a member can be removed and added to be updated - // step 1: propose to modify multisig params - public fun propose_modify( + // step 1: propose to change the name + public fun propose_name( + multisig: &mut Multisig, + key: String, + execution_time: u64, + expiration_epoch: u64, + description: String, + name: String, + ctx: &mut TxContext + ) { + let proposal_mut = multisig.create_proposal( + Witness {}, + key, + execution_time, + expiration_epoch, + description, + ctx + ); + new_name(proposal_mut, name); + } + + // step 2: multiple members have to approve the proposal (multisig::approve_proposal) + + // step 3: execute the action and modify Multisig object + public fun execute_name( + mut executable: Executable, + multisig: &mut Multisig, + ) { + name(&mut executable, multisig, Witness {}, 0); + destroy_name(&mut executable, Witness {}); + executable.destroy(Witness {}); + } + + // step 1: propose to modify multisig rules (everything touching weights) + // all vectors can be empty (if to_modify || weights are empty, the other one must be too) + // a member can be added and modified in the same proposal + // threshold has to be valid (reachable and different from 0) + public fun propose_modify_rules( multisig: &mut Multisig, key: String, execution_time: u64, expiration_epoch: u64, description: String, - name: Option, threshold: Option, - to_remove: vector
, to_add: vector
, + to_remove: vector
, + to_modify: vector
, weights: vector, ctx: &mut TxContext ) { - verify_new_config(multisig, threshold, to_remove, to_add, weights); + verify_new_rules(multisig, threshold, to_add, to_remove, to_modify, weights); let proposal_mut = multisig.create_proposal( Witness {}, key, @@ -68,18 +127,64 @@ module kraken::config { description, ctx ); - new_modify(proposal_mut, name, threshold, to_remove, to_add, weights); + // must execute add members before modify weights in case we add and modify the same member + new_members(proposal_mut, to_add, to_remove); + new_weights(proposal_mut, threshold, to_modify, weights); } // step 2: multiple members have to approve the proposal (multisig::approve_proposal) - + + // step 3: execute the action and modify Multisig object + public fun execute_modify_rules( + mut executable: Executable, + multisig: &mut Multisig, + ) { + members(&mut executable, multisig, Witness {}, 0); + destroy_members(&mut executable, Witness {}); + weights(&mut executable, multisig, Witness {}, 1); + destroy_weights(&mut executable, Witness {}); + executable.destroy(Witness {}); + } + + // step 1: propose to add or remove roles for members + public fun propose_roles( + multisig: &mut Multisig, + key: String, + execution_time: u64, + expiration_epoch: u64, + description: String, + add_to_addr: vector
, + roles_to_add: vector>, + remove_to_addr: vector
, + roles_to_remove: vector>, + ctx: &mut TxContext + ) { + let proposal_mut = multisig.create_proposal( + Witness {}, + key, + execution_time, + expiration_epoch, + description, + ctx + ); + new_roles( + proposal_mut, + add_to_addr, + roles_to_add, + remove_to_addr, + roles_to_remove + ); + } + + // step 2: multiple members have to approve the proposal (multisig::approve_proposal) + // step 3: execute the action and modify Multisig object - public fun execute_modify( + public fun execute_roles( mut executable: Executable, multisig: &mut Multisig, ) { - modify(&mut executable, multisig, Witness {}, 0); - destroy_modify(&mut executable, Witness {}); + roles(&mut executable, multisig, Witness {}, 0); + destroy_roles(&mut executable, Witness {}); executable.destroy(Witness {}); } @@ -119,46 +224,141 @@ module kraken::config { // === [ACTION] Public functions === - public fun new_modify( + public fun new_name(proposal: &mut Proposal, name: String) { + proposal.add_action(Name { name }); + } + + public fun name( + executable: &mut Executable, + multisig: &mut Multisig, + witness: W, + idx: u64, + ) { + multisig.assert_executed(executable); + let name_mut: &mut Name = executable.action_mut(witness, idx); + assert!(!name_mut.name.is_empty(), ENameAlreadySet); + multisig.set_name(name_mut.name); + name_mut.name = string::utf8(b""); // reset to confirm execution + } + + public fun destroy_name( + executable: &mut Executable, + witness: W, + ) { + let Name { name } = executable.remove_action(witness); + assert!(name.is_empty(), ENameNotSet); + } + + public fun new_members( proposal: &mut Proposal, - name: Option, - threshold: Option, + to_add: vector
, to_remove: vector
, - to_add: vector
, - weights: vector, ) { - proposal.add_action(Modify { name, threshold, to_add, weights, to_remove }); + proposal.add_action(Members { to_remove, to_add }); } - public fun modify( + public fun members( executable: &mut Executable, multisig: &mut Multisig, witness: W, idx: u64, ) { multisig.assert_executed(executable); - let modify_mut: &mut Modify = executable.action_mut(witness, idx); + let members_mut: &mut Members = executable.action_mut(witness, idx); + + multisig.remove_members(&mut members_mut.to_remove); + multisig.add_members(&mut members_mut.to_add); + } + + public fun destroy_members( + executable: &mut Executable, + witness: W + ) { + let Members { to_add, to_remove } = executable.remove_action(witness); + assert!( + to_remove.is_empty() && to_add.is_empty(), + EMembersNotExecuted + ); + } - if (modify_mut.name.is_some()) multisig.set_name(modify_mut.name.extract()); - if (modify_mut.threshold.is_some()) multisig.set_threshold(modify_mut.threshold.extract()); - multisig.remove_members(modify_mut.to_remove); - multisig.add_members(modify_mut.to_add, modify_mut.weights); + public fun new_weights( + proposal: &mut Proposal, + threshold: Option, + addresses: vector
, + weights: vector, + ) { + proposal.add_action(Weights { threshold, addresses, weights }); + } + + public fun weights( + executable: &mut Executable, + multisig: &mut Multisig, + witness: W, + idx: u64, + ) { + multisig.assert_executed(executable); + let weights_mut: &mut Weights = executable.action_mut(witness, idx); - // @dev We need to reset the vectors here. Because add/remove members copy the vector
- modify_mut.to_remove = vector[]; - modify_mut.to_add = vector[]; - modify_mut.weights = vector[]; + if (weights_mut.threshold.is_some()) { + multisig.set_threshold(weights_mut.threshold.extract()); + }; + + multisig.modify_weights(&mut weights_mut.addresses, &mut weights_mut.weights); } - public fun destroy_modify(executable: &mut Executable, witness: W) { - let Modify { name, threshold, to_remove, to_add, weights } = executable.remove_action(witness); + public fun destroy_weights( + executable: &mut Executable, + witness: W + ) { + let Weights { threshold, addresses, weights } = executable.remove_action(witness); - assert!(name.is_none() && + assert!( threshold.is_none() && - to_remove.is_empty() && - to_add.is_empty() && + addresses.is_empty() && weights.is_empty(), - EModifyNotExecuted + EWeightsNotExecuted + ); + } + + public fun new_roles( + proposal: &mut Proposal, + addr_to_add: vector
, + roles_to_add: vector>, + addr_to_remove: vector
, + roles_to_remove: vector>, + ) { + proposal.add_action(Roles { + to_add: vec_map::from_keys_values(addr_to_add, roles_to_add), + to_remove: vec_map::from_keys_values(addr_to_remove, roles_to_remove) + }); + } + + public fun roles( + executable: &mut Executable, + multisig: &mut Multisig, + witness: W, + idx: u64, + ) { + multisig.assert_executed(executable); + let roles_mut: &mut Roles = executable.action_mut(witness, idx); + + let (mut addr_to_add, mut roles_to_add) = roles_mut.to_add.into_keys_values(); + multisig.add_roles(&mut addr_to_add, &mut roles_to_add); + roles_mut.to_add = vec_map::empty(); + let (mut addr_to_remove, mut roles_to_remove) = roles_mut.to_remove.into_keys_values(); + multisig.remove_roles(&mut addr_to_remove, &mut roles_to_remove); + roles_mut.to_remove = vec_map::empty(); + } + + public fun destroy_roles( + executable: &mut Executable, + witness: W + ) { + let Roles { to_add, to_remove } = executable.remove_action(witness); + assert!( + to_remove.is_empty() && + to_add.is_empty(), + ERolesNotExecuted ); } @@ -166,7 +366,7 @@ module kraken::config { proposal.add_action(Migrate { version }); } - public fun migrate( + public fun migrate( executable: &mut Executable, multisig: &mut Multisig, witness: W, @@ -179,7 +379,7 @@ module kraken::config { migrate_mut.version = 0; // reset to 0 to enforce exactly one execution } - public fun destroy_migrate( + public fun destroy_migrate( executable: &mut Executable, witness: W, ) { @@ -187,36 +387,63 @@ module kraken::config { assert!(version == 0, EMigrateNotExecuted); } - public fun verify_new_config( + public fun verify_new_rules( multisig: &Multisig, threshold: Option, - to_remove: vector
, - to_add: vector
, - weights: vector, + mut to_add: vector
, + mut to_remove: vector
, + mut to_modify: vector
, + mut weights: vector, ) { - // verify proposed addresses match current list - let (mut i, mut added_weight) = (0, 0); - while (i < to_add.length()) { - assert!(!multisig.is_member(&to_add[i]), EAlreadyMember); - added_weight = added_weight + weights[i]; - i = i + 1; + // save addresses to add for check + let add_addr = to_add; + // verify proposed addresses match current list and save weight + let mut added_weight = 0; + while (!to_add.is_empty()) { + let addr = to_add.pop_back(); + assert!(!multisig.is_member(&addr), EAlreadyMember); + added_weight = added_weight + 1; }; - let (mut j, mut removed_weight) = (0, 0); - while (j < to_remove.length()) { - assert!(multisig.is_member(&to_remove[j]), ENotMember); - removed_weight = removed_weight + multisig.member_weight(&to_remove[j]); - j = j + 1; + let mut removed_weight = 0; + while (!to_remove.is_empty()) { + let addr = to_remove.pop_back(); + assert!(multisig.is_member(&addr), ENotMember); + removed_weight = removed_weight + multisig.member_weight(&addr); + }; + let mut add_modified_weight = 0; + let mut remove_modified_weight = 0; + while (!to_modify.is_empty()) { + let addr = to_modify.pop_back(); + assert!(multisig.is_member(&addr) || add_addr.contains(&addr), ENotMember); + let new_weight = weights.pop_back(); + let weight = if (multisig.is_member(&addr)) { + multisig.member_weight(&addr) + } else { 1 }; + + if (new_weight == weight) { + continue + } else if (new_weight > weight) { + let delta = new_weight - weight; + add_modified_weight = add_modified_weight + delta; + } else { + let delta = weight - new_weight; + remove_modified_weight = remove_modified_weight - delta; + }; }; - let new_threshold = if (threshold.is_some()) { + let mut new_threshold = multisig.threshold(); + if (threshold.is_some()) { // if threshold null, anyone can propose assert!(*threshold.borrow() > 0, EThresholdNull); - *threshold.borrow() - } else { - multisig.threshold() + new_threshold = *threshold.borrow(); }; // verify threshold is reachable with new members - let new_total_weight = multisig.total_weight() + added_weight - removed_weight; + let new_total_weight = + multisig.total_weight() + + added_weight + - removed_weight + + add_modified_weight + - remove_modified_weight; assert!(new_total_weight >= new_threshold, EThresholdTooHigh); } } diff --git a/package/sources/actions/currency.move b/package/sources/actions/currency.move index 38e1262..7b281ad 100644 --- a/package/sources/actions/currency.move +++ b/package/sources/actions/currency.move @@ -20,6 +20,7 @@ module kraken::currency { // === Structs === + // delegated witness verifying a proposal is destroyed in the module where it was created public struct Witness has copy, drop {} // Wrapper restricting access to a TreasuryCap @@ -230,7 +231,7 @@ module kraken::currency { proposal.add_action(Burn { amount }); } - public fun burn( + public fun burn( executable: &mut Executable, lock: &mut TreasuryLock, coin: Coin, @@ -243,7 +244,7 @@ module kraken::currency { burn_mut.amount = 0; // reset to ensure it has been executed } - public fun destroy_burn(executable: &mut Executable, witness: W) { + public fun destroy_burn(executable: &mut Executable, witness: W) { let Burn { amount } = executable.remove_action(witness); assert!(amount == 0, EBurnNotExecuted); } @@ -259,7 +260,7 @@ module kraken::currency { proposal.add_action(Update { name, symbol, description, icon_url }); } - public fun update( + public fun update( executable: &mut Executable, lock: &TreasuryLock, metadata: &mut CoinMetadata, @@ -282,7 +283,7 @@ module kraken::currency { // all fields are set to none now } - public fun destroy_update(executable: &mut Executable, witness: W) { + public fun destroy_update(executable: &mut Executable, witness: W) { let Update { name, symbol, description, icon_url } = executable.remove_action(witness); //@dev Future guard - impossible to trigger now assert!(name.is_none() && symbol.is_none() && description.is_none() && icon_url.is_none(), EUpdateNotExecuted); diff --git a/package/sources/actions/kiosk.move b/package/sources/actions/kiosk.move index 38da840..04903e1 100644 --- a/package/sources/actions/kiosk.move +++ b/package/sources/actions/kiosk.move @@ -23,7 +23,8 @@ module kraken::kiosk { // === Structs === - public struct Witness has drop {} + // delegated witness verifying a proposal is destroyed in the module where it was created + public struct Witness has copy, drop {} // Wrapper restricting access to a KioskOwnerCap // doesn't have store because non-transferrable @@ -241,7 +242,7 @@ module kraken::kiosk { proposal.add_action(Take { nft_ids, recipient }); } - public fun take( + public fun take( executable: &mut Executable, multisig_kiosk: &mut Kiosk, lock: &KioskOwnerLock, @@ -273,7 +274,7 @@ module kraken::kiosk { policy.confirm_request(request); } - public fun destroy_take(executable: &mut Executable, witness: W): address { + public fun destroy_take(executable: &mut Executable, witness: W): address { let Take { nft_ids, recipient } = executable.remove_action(witness); assert!(nft_ids.is_empty(), ETransferAllNftsBefore); recipient @@ -284,7 +285,7 @@ module kraken::kiosk { proposal.add_action(List { nft_ids, prices }); } - public fun list( + public fun list( executable: &mut Executable, kiosk: &mut Kiosk, lock: &KioskOwnerLock, @@ -297,7 +298,7 @@ module kraken::kiosk { kiosk.list(&lock.kiosk_owner_cap, nft_id, price); } - public fun destroy_list(executable: &mut Executable, witness: W) { + public fun destroy_list(executable: &mut Executable, witness: W) { let List { nft_ids, prices: _ } = executable.remove_action(witness); assert!(nft_ids.is_empty(), EListAllNftsBefore); } diff --git a/package/sources/actions/payments.move b/package/sources/actions/payments.move index e9cc1fc..e67df62 100644 --- a/package/sources/actions/payments.move +++ b/package/sources/actions/payments.move @@ -20,6 +20,7 @@ module kraken::payments { // === Structs === + // delegated witness verifying a proposal is destroyed in the module where it was created public struct Witness has copy, drop {} // [ACTION] diff --git a/package/sources/actions/transfers.move b/package/sources/actions/transfers.move index 5b1c13a..fb2493e 100644 --- a/package/sources/actions/transfers.move +++ b/package/sources/actions/transfers.move @@ -22,7 +22,7 @@ module kraken::transfers { // === Structs === - // witness verifying a proposal is destroyed in the module where it was created + // delegated witness verifying a proposal is destroyed in the module where it was created public struct Witness has copy, drop {} // [ACTION] @@ -95,7 +95,7 @@ module kraken::transfers { executable.destroy(Witness {}); } - // // step 1: propose to deliver object to a recipient that must claim it + // step 1: propose to deliver object to a recipient that must claim it public fun propose_delivery( multisig: &mut Multisig, key: String, diff --git a/package/sources/actions/upgrade_policies.move b/package/sources/actions/upgrade_policies.move index 2874146..e33c380 100644 --- a/package/sources/actions/upgrade_policies.move +++ b/package/sources/actions/upgrade_policies.move @@ -25,7 +25,8 @@ module kraken::upgrade_policies { // === Structs === - public struct Witness has drop {} + // delegated witness verifying a proposal is destroyed in the module where it was created + public struct Witness has copy, drop {} // [ACTION] public struct Upgrade has store { @@ -223,7 +224,7 @@ module kraken::upgrade_policies { proposal.add_action(Upgrade { digest, lock_id }); } - public fun upgrade( + public fun upgrade( executable: &mut Executable, lock: &mut UpgradeLock, witness: W, @@ -240,7 +241,7 @@ module kraken::upgrade_policies { ticket } - public fun destroy_upgrade(executable: &mut Executable, witness: W) { + public fun destroy_upgrade(executable: &mut Executable, witness: W) { let Upgrade { digest, lock_id: _ } = executable.remove_action(witness); assert!(digest.is_empty(), EUpgradeNotExecuted); } @@ -258,7 +259,7 @@ module kraken::upgrade_policies { proposal.add_action(Restrict { policy, lock_id: object::id(lock) }); } - public fun restrict( + public fun restrict( executable: &mut Executable, multisig: &mut Multisig, mut lock: UpgradeLock, @@ -285,7 +286,7 @@ module kraken::upgrade_policies { restrict_mut.policy = 0; } - public fun destroy_restrict(executable: &mut Executable, witness: W) { + public fun destroy_restrict(executable: &mut Executable, witness: W) { let Restrict { policy, lock_id: _ } = executable.remove_action(witness); assert!(policy == 0, ERestrictNotExecuted); } diff --git a/package/sources/multisig.move b/package/sources/multisig.move index 0989c87..36083d1 100644 --- a/package/sources/multisig.move +++ b/package/sources/multisig.move @@ -13,7 +13,7 @@ module kraken::multisig { use std::{ - string::String, + string::{Self, String}, type_name::{Self, TypeName} }; use sui::{ @@ -34,6 +34,7 @@ module kraken::multisig { const ENotMultisigExecutable: u64 = 6; const EProposalNotFound: u64 = 7; const EMemberNotFound: u64 = 8; + const ERoleNotFound: u64 = 9; // === Constants === @@ -64,6 +65,8 @@ module kraken::multisig { weight: u64, // ID of the member's account, none if he didn't join yet account_id: Option, + // roles that have been attributed + roles: VecSet, } // proposal owning a single action requested to be executed @@ -93,7 +96,7 @@ module kraken::multisig { multisig_addr: address, // module that issued the proposal and must destroy it module_witness: TypeName, - // index of the next action to destroy + // index of the next action to destroy, starts at 0 next_to_destroy: u64, // actions to be executed in order actions: Bag, @@ -105,8 +108,14 @@ module kraken::multisig { // creator is added by default with weight and threshold of 1 public fun new(name: String, account_id: ID, ctx: &mut TxContext): Multisig { let mut members = vec_map::empty(); - members.insert(ctx.sender(), Member { weight: 1, account_id: option::some(account_id) }); - + members.insert( + ctx.sender(), + Member { + weight: 1, + account_id: option::some(account_id), + roles: vec_set::empty() + } + ); Multisig { id: object::new(ctx), version: VERSION, @@ -221,7 +230,44 @@ module kraken::multisig { Executable { multisig_addr: multisig.id.uid_to_inner().id_to_address(), - module_witness: module_witness, + module_witness, + next_to_destroy: 0, + actions + } + } + + // return an executable if the number of signers is >= threshold + public fun execute_proposal_with_role( + multisig: &mut Multisig, + key: String, + clock: &Clock, + ctx: &mut TxContext + ): Executable { + multisig.assert_is_member(ctx); + multisig.assert_version(); + + let (_, proposal) = multisig.proposals.remove(&key); + let Proposal { + id, + module_witness, + description: _, + expiration_epoch: _, + execution_time, + actions, + approval_weight: _, + approved: _, + } = proposal; + id.delete(); + + assert!( + multisig.members.get(&ctx.sender()).roles.contains(&string::from_ascii(module_witness.into_string())), + ERoleNotFound + ); + assert!(clock.timestamp_ms() >= execution_time, ECantBeExecutedYet); + + Executable { + multisig_addr: multisig.id.uid_to_inner().id_to_address(), + module_witness, next_to_destroy: 0, actions } @@ -325,6 +371,16 @@ module kraken::multisig { let member = multisig.members.get(addr); member.weight } + + public fun member_account_id(multisig: &Multisig, addr: &address): Option { + let member = multisig.members.get(addr); + member.account_id + } + + public fun member_roles(multisig: &Multisig, addr: &address): vector { + let member = multisig.members.get(addr); + *member.roles.keys() + } public fun is_member(multisig: &Multisig, addr: &address): bool { multisig.members.contains(addr) @@ -334,11 +390,6 @@ module kraken::multisig { assert!(multisig.members.contains(&ctx.sender()), ECallerIsNotMember); } - public fun member_account_id(multisig: &Multisig, addr: &address): Option { - let member = multisig.members.get(addr); - member.account_id - } - public fun proposals_length(multisig: &Multisig): u64 { multisig.proposals.size() } @@ -412,28 +463,86 @@ module kraken::multisig { // callable only in config.move, if the proposal has been accepted public(package) fun add_members( multisig: &mut Multisig, - mut addresses: vector
, - mut weights: vector + addresses: &mut vector
, ) { while (addresses.length() > 0) { + multisig.total_weight = multisig.total_weight + 1; let addr = addresses.pop_back(); - let weight = weights.pop_back(); - multisig.total_weight = multisig.total_weight + weight; multisig.members.insert( addr, - Member { weight, account_id: option::none() } + Member { weight: 1, account_id: option::none(), roles: vec_set::empty() } ); - } + }; } // callable only in config.move, if the proposal has been accepted - public(package) fun remove_members(multisig: &mut Multisig, mut addresses: vector
) { + public(package) fun remove_members(multisig: &mut Multisig, addresses: &mut vector
) { while (addresses.length() > 0) { let addr = addresses.pop_back(); let (_, member) = multisig.members.remove(&addr); - let Member { weight , account_id: _ } = member; + let Member { weight, account_id: _, roles: _ } = member; multisig.total_weight = multisig.total_weight - weight; - } + }; + } + + // callable only in config.move, if the proposal has been accepted + public(package) fun modify_weights( + multisig: &mut Multisig, + addresses: &mut vector
, + weights: &mut vector + ) { + while (addresses.length() > 0) { + let addr = addresses.pop_back(); + let new_weight = weights.pop_back(); + let weigth = multisig.members[&addr].weight; + + if (new_weight == weigth) { + continue + } else if (new_weight > weigth) { + let delta = new_weight - weigth; + multisig.total_weight = multisig.total_weight + delta; + } else { + let delta = weigth - new_weight; + multisig.total_weight = multisig.total_weight - delta; + }; + multisig.members[&addr].weight = new_weight; + }; + } + + // callable only in config.move, if the proposal has been accepted + public(package) fun add_roles( + multisig: &mut Multisig, + addresses: &mut vector
, + roles: &mut vector> + ) { + while (addresses.length() > 0) { + let addr = addresses.pop_back(); + let mut to_add = roles.pop_back(); + let member = multisig.members.get_mut(&addr); + + while (!to_add.is_empty()) { + let role = to_add.pop_back(); + member.roles.insert(role); + } + }; + } + + // callable only in config.move, if the proposal has been accepted + public(package) fun remove_roles( + multisig: &mut Multisig, + addresses: &mut vector
, + roles: &mut vector> + ) { + while (addresses.length() > 0) { + let addr = addresses.pop_back(); + let mut to_remove = roles.pop_back(); + let member = multisig.members.get_mut(&addr); + + while (!to_remove.is_empty()) { + let role = to_remove.pop_back(); + member.roles.remove(&role); + } + }; } // for adding account id to members, from account.move diff --git a/package/tests/account_tests.move b/package/tests/account_tests.move index 2df6f17..c342882 100644 --- a/package/tests/account_tests.move +++ b/package/tests/account_tests.move @@ -52,7 +52,7 @@ module kraken::account_tests { let mut user_account = world.scenario().take_from_address(ALICE); - world.multisig().add_members(vector[ALICE], vector[2]); + world.multisig().add_members(&mut vector[ALICE]); world.join_multisig(&mut user_account); assert_eq(user_account.multisig_ids(), vector[object::id(world.multisig())]); @@ -77,7 +77,7 @@ module kraken::account_tests { let mut user_account = world.scenario().take_from_address(ALICE); - world.multisig().add_members(vector[ALICE], vector[2]); + world.multisig().add_members(&mut vector[ALICE]); assert_eq(user_account.multisig_ids(), vector[]); @@ -109,7 +109,7 @@ module kraken::account_tests { let user_account = world.scenario().take_from_address(ALICE); - world.multisig().add_members(vector[ALICE], vector[2]); + world.multisig().add_members(&mut vector[ALICE]); assert_eq(user_account.multisig_ids(), vector[]); diff --git a/package/tests/config_tests.move b/package/tests/config_tests.move index 3b0188c..48f30e3 100644 --- a/package/tests/config_tests.move +++ b/package/tests/config_tests.move @@ -11,6 +11,33 @@ module kraken::config_tests{ const ALICE: address = @0xA11CE; const BOB: address = @0xB0B; + #[test] + fun test_name_end_to_end() { + let mut world = start_world(); + let key = utf8(b"name proposal"); + + world.propose_name( + key, + 1, + 2, + utf8(b"description"), + utf8(b"new name"), + ); + + world.approve_proposal(key); + + world.scenario().next_tx(OWNER); + world.scenario().next_epoch(OWNER); + world.scenario().next_epoch(OWNER); + world.clock().increment_for_testing(2); + + let executable = world.execute_proposal(key); + config::execute_name(executable, world.multisig()); + + assert_eq(world.multisig().name(), utf8(b"new name")); + world.end(); + } + #[test] #[allow(implicit_const_copy)] fun test_modify_end_to_end() { @@ -26,16 +53,16 @@ module kraken::config_tests{ assert_eq(multisig.proposals_length(), 0); assert_eq(multisig.total_weight(), 1); - world.propose_modify( + world.propose_modify_rules( key, 1, 2, utf8(b"description"), - option::some(utf8(b"update1")), option::some(3), - vector[OWNER], vector[ALICE, BOB], - vector[2, 1] + vector[OWNER], + vector[ALICE], + vector[2] ); world.approve_proposal(key); @@ -47,11 +74,10 @@ module kraken::config_tests{ let executable = world.execute_proposal(key); - config::execute_modify(executable, world.multisig()); + config::execute_modify_rules(executable, world.multisig()); let multisig = world.multisig(); - assert_eq(multisig.name(), utf8(b"update1")); assert_eq(multisig.threshold(), 3); assert_eq(multisig.member_addresses(), vector[BOB, ALICE]); assert_eq(multisig.total_weight(), 3); @@ -61,11 +87,92 @@ module kraken::config_tests{ world.end(); } + #[test] + #[allow(implicit_const_copy)] + fun test_roles_end_to_end() { + let mut world = start_world(); + + let sender = world.scenario().ctx().sender(); + let key = utf8(b"roles proposal"); + + assert_eq(world.multisig().member_roles(&sender), vector[]); + + // add role + let mut role = @kraken.to_string(); + role.append_utf8(b"::config::Witness"); + world.propose_roles( + key, + 1, + 2, + utf8(b"description"), + vector[OWNER], + vector[vector[role]], + vector[], + vector[] + ); + + world.approve_proposal(key); + + world.scenario().next_tx(OWNER); + world.scenario().next_epoch(OWNER); + world.scenario().next_epoch(OWNER); + world.clock().increment_for_testing(2); + let executable = world.execute_proposal(key); + config::execute_roles(executable, world.multisig()); + assert_eq(world.multisig().member_roles(&sender), vector[role]); + + // execute action with role + world.propose_name( + key, + 1, + 2, + utf8(b"description"), + utf8(b"new name"), + ); + + world.approve_proposal(key); + + world.scenario().next_tx(OWNER); + world.scenario().next_epoch(OWNER); + world.scenario().next_epoch(OWNER); + world.clock().increment_for_testing(2); + + let executable = world.execute_proposal(key); + config::execute_name(executable, world.multisig()); + assert_eq(world.multisig().name(), utf8(b"new name")); + + // remove role + let mut role = @kraken.to_string(); + role.append_utf8(b"::config::Witness"); + world.propose_roles( + key, + 1, + 2, + utf8(b"description"), + vector[], + vector[], + vector[OWNER], + vector[vector[role]], + ); + + world.approve_proposal(key); + + world.scenario().next_tx(OWNER); + world.scenario().next_epoch(OWNER); + world.scenario().next_epoch(OWNER); + world.clock().increment_for_testing(2); + let executable = world.execute_proposal(key); + config::execute_roles(executable, world.multisig()); + assert_eq(world.multisig().member_roles(&sender), vector[]); + + world.end(); + } + #[test] fun test_migrate_end_to_end() { let mut world = start_world(); - let key = utf8(b"modify proposal"); + let key = utf8(b"migrate proposal"); assert_eq(world.multisig().version(), 1); @@ -92,23 +199,23 @@ module kraken::config_tests{ world.end(); } - + #[test] - #[expected_failure(abort_code = config::EThresholdTooHigh)] - fun test_verify_new_config_error_threshold_too_high() { + #[expected_failure(abort_code = config::EAlreadyMember)] + fun test_verify_new_config_error_added_already_member() { let mut world = start_world(); let key = utf8(b"modify proposal"); - world.propose_modify( + world.propose_modify_rules( key, 1, 2, utf8(b"description"), - option::some(utf8(b"update1")), - option::some(4), + option::some(2), vector[OWNER], - vector[ALICE, BOB], - vector[2, 1] + vector[], + vector[], + vector[], ); world.approve_proposal(key); @@ -120,27 +227,27 @@ module kraken::config_tests{ let executable = world.execute_proposal(key); - config::execute_modify(executable, world.multisig()); + config::execute_modify_rules(executable, world.multisig()); world.end(); - } + } #[test] #[expected_failure(abort_code = config::ENotMember)] - fun test_verify_new_config_error_not_member() { + fun test_verify_new_config_error_removed_not_member() { let mut world = start_world(); let key = utf8(b"modify proposal"); - world.propose_modify( + world.propose_modify_rules( key, 1, 2, utf8(b"description"), - option::some(utf8(b"update1")), option::some(2), + vector[], vector[ALICE], - vector[ALICE, BOB], - vector[2, 1] + vector[], + vector[] ); world.approve_proposal(key); @@ -152,27 +259,59 @@ module kraken::config_tests{ let executable = world.execute_proposal(key); - config::execute_modify(executable, world.multisig()); + config::execute_modify_rules(executable, world.multisig()); world.end(); } - + #[test] - #[expected_failure(abort_code = config::EAlreadyMember)] - fun test_verify_new_config_error_already_member() { + #[expected_failure(abort_code = config::ENotMember)] + fun test_verify_new_config_error_modified_not_member() { let mut world = start_world(); let key = utf8(b"modify proposal"); - world.propose_modify( + world.propose_modify_rules( key, 1, 2, utf8(b"description"), - option::some(utf8(b"update1")), option::some(2), vector[], + vector[], + vector[ALICE], + vector[2] + ); + + world.approve_proposal(key); + + world.scenario().next_tx(OWNER); + world.scenario().next_epoch(OWNER); + world.scenario().next_epoch(OWNER); + world.clock().increment_for_testing(2); + + let executable = world.execute_proposal(key); + + config::execute_modify_rules(executable, world.multisig()); + + world.end(); + } + + #[test] + #[expected_failure(abort_code = config::EThresholdTooHigh)] + fun test_verify_new_config_error_threshold_too_high() { + let mut world = start_world(); + let key = utf8(b"modify proposal"); + + world.propose_modify_rules( + key, + 1, + 2, + utf8(b"description"), + option::some(4), + vector[ALICE, BOB], vector[OWNER], - vector[2, 1] + vector[ALICE], + vector[2] ); world.approve_proposal(key); @@ -184,10 +323,10 @@ module kraken::config_tests{ let executable = world.execute_proposal(key); - config::execute_modify(executable, world.multisig()); + config::execute_modify_rules(executable, world.multisig()); world.end(); - } + } #[test] #[expected_failure(abort_code = config::EThresholdNull)] @@ -195,16 +334,16 @@ module kraken::config_tests{ let mut world = start_world(); let key = utf8(b"modify proposal"); - world.propose_modify( + world.propose_modify_rules( key, 1, 2, utf8(b"description"), - option::some(utf8(b"update1")), option::some(0), vector[], - vector[ALICE], - vector[2, 1] + vector[], + vector[], + vector[], ); world.approve_proposal(key); @@ -216,7 +355,7 @@ module kraken::config_tests{ let executable = world.execute_proposal(key); - config::execute_modify(executable, world.multisig()); + config::execute_modify_rules(executable, world.multisig()); world.end(); } diff --git a/package/tests/multisig_tests.move b/package/tests/multisig_tests.move index a966def..a1025c3 100644 --- a/package/tests/multisig_tests.move +++ b/package/tests/multisig_tests.move @@ -70,8 +70,11 @@ module kraken::multisig_tests { let mut world = start_world(); world.multisig().add_members( - vector[ALICE, BOB], - vector[2, 3] + &mut vector[ALICE, BOB], + ); + world.multisig().modify_weights( + &mut vector[ALICE, BOB], + &mut vector[2, 3] ); let proposal = world.create_proposal( @@ -117,8 +120,11 @@ module kraken::multisig_tests { let mut world = start_world(); world.multisig().add_members( - vector[ALICE, BOB], - vector[2, 3] + &mut vector[ALICE, BOB], + ); + world.multisig().modify_weights( + &mut vector[ALICE, BOB], + &mut vector[2, 3] ); let proposal = world.create_proposal( @@ -221,8 +227,11 @@ module kraken::multisig_tests { assert_eq(world.multisig().is_member(&BOB), false); world.multisig().add_members( - vector[ALICE, BOB], - vector[2, 3] + &mut vector[ALICE, BOB], + ); + world.multisig().modify_weights( + &mut vector[ALICE, BOB], + &mut vector[2, 3] ); assert_eq(world.multisig().is_member(&ALICE), true); @@ -253,7 +262,7 @@ module kraken::multisig_tests { world.scenario().next_tx(OWNER); - world.multisig().remove_members(vector[ALICE]); + world.multisig().remove_members(&mut vector[ALICE]); assert_eq(world.multisig().is_member(&ALICE), false); @@ -278,8 +287,11 @@ module kraken::multisig_tests { let mut world = start_world(); world.multisig().add_members( - vector[ALICE, BOB], - vector[1, 3] + &mut vector[ALICE, BOB], + ); + world.multisig().modify_weights( + &mut vector[ALICE, BOB], + &mut vector[1, 3] ); let key = utf8(b"key"); @@ -310,8 +322,11 @@ module kraken::multisig_tests { let mut world = start_world(); world.multisig().add_members( - vector[ALICE, BOB], - vector[1, 3] + &mut vector[ALICE, BOB], + ); + world.multisig().modify_weights( + &mut vector[ALICE, BOB], + &mut vector[2, 3] ); let key = utf8(b"key"); @@ -342,8 +357,11 @@ module kraken::multisig_tests { let mut world = start_world(); world.multisig().add_members( - vector[ALICE, BOB], - vector[1, 3] + &mut vector[ALICE, BOB], + ); + world.multisig().modify_weights( + &mut vector[ALICE, BOB], + &mut vector[2, 3] ); let key = utf8(b"key"); @@ -414,8 +432,11 @@ module kraken::multisig_tests { let mut world = start_world(); world.multisig().add_members( - vector[ALICE, BOB], - vector[1, 3] + &mut vector[ALICE, BOB], + ); + world.multisig().modify_weights( + &mut vector[ALICE, BOB], + &mut vector[2, 3] ); let key = utf8(b"key"); diff --git a/package/tests/utils.move b/package/tests/utils.move index 7ac4a81..7be9e6f 100644 --- a/package/tests/utils.move +++ b/package/tests/utils.move @@ -1,7 +1,8 @@ #[test_only] module kraken::test_utils { - use std::string::{Self, String}; - + use std::{ + string::{Self, String}, + }; use sui::{ bag::Bag, test_utils::destroy, @@ -13,7 +14,6 @@ module kraken::test_utils { transfer_policy::TransferPolicy, test_scenario::{Self as ts, Scenario, receiving_ticket_by_id, most_recent_id_for_address}, }; - use kraken::{ owned, config, @@ -43,6 +43,10 @@ module kraken::test_utils { // === Utils === + public fun wtf(): u64 { + 1 + } + public fun start_world(): World { let mut scenario = ts::begin(OWNER); account::new(string::utf8(b"sam"), string::utf8(b"move_god.png"), scenario.ctx()); @@ -223,33 +227,77 @@ module kraken::test_utils { multisig::assert_is_member(&world.multisig, world.scenario.ctx()); } - public fun propose_modify( + public fun propose_name( + world: &mut World, + key: String, + execution_time: u64, + expiration_epoch: u64, + description: String, + name: String + ) { + config::propose_name( + &mut world.multisig, + key, + execution_time, + expiration_epoch, + description, + name, + world.scenario.ctx() + ); + } + + public fun propose_modify_rules( world: &mut World, key: String, execution_time: u64, expiration_epoch: u64, description: String, - name: Option, threshold: Option, - to_remove: vector
, to_add: vector
, + to_remove: vector
, + to_modify: vector
, weights: vector ) { - config::propose_modify( + config::propose_modify_rules( &mut world.multisig, key, execution_time, expiration_epoch, description, - name, threshold, - to_remove, to_add, + to_remove, + to_modify, weights, world.scenario.ctx() ); } + public fun propose_roles( + world: &mut World, + key: String, + execution_time: u64, + expiration_epoch: u64, + description: String, + addr_to_add: vector
, + roles_to_add: vector>, + addr_to_remove: vector
, + roles_to_remove: vector>, + ) { + config::propose_roles( + &mut world.multisig, + key, + execution_time, + expiration_epoch, + description, + addr_to_add, + roles_to_add, + addr_to_remove, + roles_to_remove, + world.scenario.ctx() + ); + } + public fun propose_migrate( world: &mut World, key: String,