diff --git a/identity_iota_core/packages/iota_identity/sources/asset.move b/identity_iota_core/packages/iota_identity/sources/asset.move index 5747bf21a..40ed3703b 100644 --- a/identity_iota_core/packages/iota_identity/sources/asset.move +++ b/identity_iota_core/packages/iota_identity/sources/asset.move @@ -124,7 +124,7 @@ module iota_identity::asset { transfer::share_object(proposal); } - /// Strucure that encodes the logic required to transfer an `AuthenticatedAsset` + /// Structure that encodes the logic required to transfer an `AuthenticatedAsset` /// from one address to another. The transfer can be refused by the recipient. public struct TransferProposal has key { id: UID, diff --git a/identity_iota_core/packages/iota_identity/sources/controller.move b/identity_iota_core/packages/iota_identity/sources/controller.move new file mode 100644 index 000000000..ae39130d5 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/controller.move @@ -0,0 +1,144 @@ +module iota_identity::controller { + use iota::transfer::Receiving; + use iota::borrow::{Self, Referent, Borrow}; + use iota_identity::permissions; + + public use fun delete_controller_cap as ControllerCap.delete; + public use fun delete_delegation_token as DelegationToken.delete; + + /// This `ControllerCap` cannot delegate access. + const ECannotDelegate: u64 = 0; + // The permission of the provided `DelegationToken` are not + // valid to perform this operation. + const EInvalidPermissions: u64 = 1; + + /// Event that is created when a new `DelegationToken` is minted. + public struct NewDelegationTokenEvent has copy, drop { + controller: ID, + token: ID, + permissions: u32, + } + + /// Capability that allows to access mutative APIs of a `Multicontroller`. + public struct ControllerCap has key { + id: UID, + can_delegate: bool, + access_token: Referent, + } + + public fun id(self: &ControllerCap): &UID { + &self.id + } + + /// Borrows this `ControllerCap`'s access token. + public fun borrow(self: &mut ControllerCap): (DelegationToken, Borrow) { + self.access_token.borrow() + } + + /// Returns the borrowed access token together with the hot potato. + public fun put_back(self: &mut ControllerCap, token: DelegationToken, borrow: Borrow) { + self.access_token.put_back(token, borrow); + } + + /// Creates a delegation token for this controller. The created `DelegationToken` + /// will have full permissions. Use `delegate_with_permissions` to set or unset + /// specific permissions. + public fun delegate(self: &ControllerCap, ctx: &mut TxContext): DelegationToken { + assert!(self.can_delegate, ECannotDelegate); + new_delegation_token(self.id.to_inner(), permissions::all(), ctx) + } + + /// Creates a delegation token for this controller, specifying the delegate's permissions. + public fun delegate_with_permissions(self: &ControllerCap, permissions: u32, ctx: &mut TxContext): DelegationToken { + assert!(self.can_delegate, ECannotDelegate); + new_delegation_token(self.id.to_inner(), permissions, ctx) + } + + /// A token that allows an entity to act in a Controller's stead. + public struct DelegationToken has key, store { + id: UID, + permissions: u32, + controller: ID, + } + + /// Returns the controller's ID of this `DelegationToken`. + public fun controller(self: &DelegationToken): ID { + self.controller + } + + /// Returns the permissions of this `DelegationToken`. + public fun permissions(self: &DelegationToken): u32 { + self.permissions + } + + /// Returns true if this `DelegationToken` has permission `permission`. + public fun has_permission(self: &DelegationToken, permission: u32): bool { + self.permissions & permission != 0 + } + + /// Aborts if this `DelegationToken` doesn't have permission `permission`. + public fun assert_has_permission(self: &DelegationToken, permission: u32) { + assert!(self.has_permission(permission), EInvalidPermissions) + } + + /// Creates a new `ControllerCap`. + public(package) fun new(can_delegate: bool, ctx: &mut TxContext): ControllerCap { + let id = object::new(ctx); + let access_token = borrow::new(new_delegation_token(id.to_inner(), permissions::all(), ctx), ctx); + + ControllerCap { + id, + access_token, + can_delegate, + } + } + + /// Transfer a `ControllerCap`. + public(package) fun transfer(cap: ControllerCap, recipient: address) { + transfer::transfer(cap, recipient) + } + + /// Receives a `ControllerCap`. + public(package) fun receive(owner: &mut UID, cap: Receiving): ControllerCap { + transfer::receive(owner, cap) + } + + public(package) fun new_delegation_token( + controller: ID, + permissions: u32, + ctx: &mut TxContext + ): DelegationToken { + let id = object::new(ctx); + + iota::event::emit(NewDelegationTokenEvent { + controller, + token: id.to_inner(), + permissions, + }); + + DelegationToken { + id, + controller, + permissions, + } + } + + public(package) fun delete_controller_cap(cap: ControllerCap) { + let ControllerCap { + access_token, + id, + .. + } = cap; + + delete_delegation_token(access_token.destroy()); + object::delete(id); + } + + public(package) fun delete_delegation_token(token: DelegationToken) { + let DelegationToken { + id, + .. + } = token; + object::delete(id); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move index 0e68188df..592c624ea 100644 --- a/identity_iota_core/packages/iota_identity/sources/identity.move +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -8,7 +8,8 @@ module iota_identity::identity { clock::Clock, }; use iota_identity::{ - multicontroller::{Self, ControllerCap, Multicontroller, Action}, + multicontroller::{Self, Multicontroller, Action}, + controller::{DelegationToken, ControllerCap}, update_value_proposal, config_proposal, transfer_proposal::{Self, Send}, @@ -41,7 +42,7 @@ module iota_identity::identity { clock: &Clock, ctx: &mut TxContext ): Identity { - new_with_controller(doc, ctx.sender(), clock, ctx) + new_with_controller(doc, ctx.sender(), false, clock, ctx) } /// Creates an identity specifying its `created` timestamp. @@ -52,7 +53,7 @@ module iota_identity::identity { clock: &Clock, ctx: &mut TxContext ): Identity { - let mut identity = new_with_controller(doc, ctx.sender(), clock, ctx); + let mut identity = new_with_controller(doc, ctx.sender(), false, clock, ctx); assert!(identity.updated >= creation_timestamp, EInvalidTimestamp); identity.created = creation_timestamp; @@ -64,13 +65,14 @@ module iota_identity::identity { public fun new_with_controller( doc: vector, controller: address, + can_delegate: bool, clock: &Clock, ctx: &mut TxContext, ): Identity { let now = clock.timestamp_ms(); Identity { id: object::new(ctx), - did_doc: multicontroller::new_with_controller(doc, controller, ctx), + did_doc: multicontroller::new_with_controller(doc, controller, can_delegate, ctx), created: now, updated: now, } @@ -82,6 +84,7 @@ module iota_identity::identity { public fun new_with_controllers( doc: vector, controllers: VecMap, + controllers_that_can_delegate: VecMap, threshold: u64, clock: &Clock, ctx: &mut TxContext, @@ -93,7 +96,7 @@ module iota_identity::identity { let now = clock.timestamp_ms(); Identity { id: object::new(ctx), - did_doc: multicontroller::new_with_controllers(doc, controllers, threshold, ctx), + did_doc: multicontroller::new_with_controllers(doc, controllers, controllers_that_can_delegate, threshold, ctx), created: now, updated: now, } @@ -124,18 +127,16 @@ module iota_identity::identity { /// Approve an `Identity`'s `Proposal`. public fun approve_proposal( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ) { self.did_doc.approve_proposal, T>(cap, proposal_id); } - /// Proposes the deativates the DID Document contained in this `Identity`. - /// This function can deactivate the DID Document right away if `cap` has - /// enough voting power. + /// Proposes the deativation of the DID Document contained in this `Identity`. public fun propose_deactivation( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, clock: &Clock, ctx: &mut TxContext, @@ -160,7 +161,7 @@ module iota_identity::identity { /// Executes a proposal to deactivate this `Identity`'s DID document. public fun execute_deactivation( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, clock: &Clock, ctx: &mut TxContext, @@ -179,7 +180,7 @@ module iota_identity::identity { /// enough voting power. public fun propose_update( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, updated_doc: vector, expiration: Option, clock: &Clock, @@ -208,7 +209,7 @@ module iota_identity::identity { /// Executes a proposal to update the DID Document contained in this `Identity`. public fun execute_update( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, clock: &Clock, ctx: &mut TxContext, @@ -228,7 +229,7 @@ module iota_identity::identity { /// has enough voting power. public fun propose_config_change( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, threshold: Option, controllers_to_add: VecMap, @@ -261,7 +262,7 @@ module iota_identity::identity { /// Execute a proposal to change this `Identity`'s AC. public fun execute_config_change( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext ) { @@ -276,7 +277,7 @@ module iota_identity::identity { /// Proposes the transfer of a set of objects owned by this `Identity`. public fun propose_send( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, objects: vector, recipients: vector
, @@ -305,7 +306,7 @@ module iota_identity::identity { /// in order to use them in a transaction. Borrowed assets must be returned. public fun propose_borrow( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, objects: vector, ctx: &mut TxContext, @@ -334,7 +335,7 @@ module iota_identity::identity { /// to add a new controller. public fun propose_new_controller( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, new_controller_addr: address, voting_power: u64, @@ -349,14 +350,35 @@ module iota_identity::identity { /// Executes an `Identity`'s proposal. public fun execute_proposal( self: &mut Identity, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ): Action { self.did_doc.execute_proposal(cap, proposal_id, ctx) } - /// Checks if `data` is a state matadata representing a DID. + /// revoke the `DelegationToken` with `ID` `deny_id`. Only controllers can perform this operation. + public fun revoke_token(self: &mut Identity, cap: &ControllerCap, deny_id: ID) { + self.did_doc.revoke_token(cap, deny_id); + } + + /// Un-revoke a `DelegationToken`. + public fun unrevoke_token(self: &mut Identity, cap: &ControllerCap, token_id: ID) { + self.did_doc.unrevoke_token(cap, token_id); + } + + /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from + /// the controller committee. + public fun destroy_controller_cap(self: &Identity, cap: ControllerCap) { + self.did_doc.destroy_controller_cap(cap); + } + + /// Destroys a `DelegationToken`. + public fun destroy_delegation_token(self: &mut Identity, token: DelegationToken) { + self.did_doc.destroy_delegation_token(token); + } + + /// Checks if `data` is a state metadata representing a DID. /// i.e. starts with the bytes b"DID". public(package) fun is_did_output(data: &vector): bool { data[0] == 0x44 && // b'D' @@ -380,7 +402,8 @@ module iota_identity::identity_tests { use iota::test_scenario; use iota_identity::identity::{new, ENotADidDocument, Identity, new_with_controllers}; use iota_identity::config_proposal::Modify; - use iota_identity::multicontroller::{ControllerCap, EExpiredProposal, EThresholdNotReached}; + use iota_identity::multicontroller::{EExpiredProposal, EThresholdNotReached}; + use iota_identity::controller::ControllerCap; use iota::vec_map; use iota::clock; @@ -401,16 +424,19 @@ module iota_identity::identity_tests { // Create a request to add a second controller. let mut identity = scenario.take_shared(); - let controller1_cap = scenario.take_from_address(controller1); + let mut controller1_cap = scenario.take_from_address(controller1); + let (token, borrow) = controller1_cap.borrow(); // This is carried out immediately. - identity.propose_new_controller(&controller1_cap, option::none(), controller2, 1, scenario.ctx()); + identity.propose_new_controller(&token, option::none(), controller2, 1, scenario.ctx()); + controller1_cap.put_back(token, borrow); scenario.next_tx(controller2); - let controller2_cap = scenario.take_from_address(controller2); - - identity.did_doc().assert_is_member(&controller2_cap); + let mut controller2_cap = scenario.take_from_address(controller2); + let (token, borrow) = controller2_cap.borrow(); + identity.did_doc().assert_is_member(&token); + controller2_cap.put_back(token, borrow); // Cleanup test_scenario::return_to_address(controller1, controller1_cap); test_scenario::return_to_address(controller2, controller2_cap); @@ -437,6 +463,7 @@ module iota_identity::identity_tests { let identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 2, &clock, scenario.ctx(), @@ -447,11 +474,12 @@ module iota_identity::identity_tests { // `controller1` creates a request to remove `controller3`. let mut identity = scenario.take_shared(); - let controller1_cap = scenario.take_from_address(controller1); + let mut controller1_cap = scenario.take_from_address(controller1); let controller3_cap = scenario.take_from_address(controller3); + let (token, borrow) = controller1_cap.borrow(); let proposal_id = identity.propose_config_change( - &controller1_cap, + &token, option::none(), option::none(), vec_map::empty(), @@ -459,17 +487,22 @@ module iota_identity::identity_tests { vec_map::empty(), scenario.ctx() ).destroy_some(); + controller1_cap.put_back(token, borrow); scenario.next_tx(controller2); // `controller2` also approves the removal of `controller3`. - let controller2_cap = scenario.take_from_address(controller2); - identity.approve_proposal(&controller2_cap, proposal_id); + let mut controller2_cap = scenario.take_from_address(controller2); + let (token, borrow) = controller2_cap.borrow(); + identity.approve_proposal(&token, proposal_id); + controller2_cap.put_back(token, borrow); scenario.next_tx(controller2); // `controller3` is removed. - identity.execute_config_change(&controller2_cap, proposal_id, scenario.ctx()); + let (token, borrow) = controller2_cap.borrow(); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller2_cap.put_back(token, borrow); assert!(!identity.did_doc().controllers().contains(&controller3_cap.id().to_inner()), 0); // cleanup. @@ -505,6 +538,7 @@ module iota_identity::identity_tests { let identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 10, &clock, scenario.ctx(), @@ -514,16 +548,20 @@ module iota_identity::identity_tests { // Controller A alone should be able to do anything. let mut identity = scenario.take_shared(); - let controller_a_cap = scenario.take_from_address(controller_a); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); // Create a request to add a new controller. This is carried out immediately as controller_a has enough voting power - identity.propose_new_controller(&controller_a_cap, option::none(), controller_d, 1, scenario.ctx()); + identity.propose_new_controller(&token, option::none(), controller_d, 1, scenario.ctx()); + controller_a_cap.put_back(token, borrow); scenario.next_tx(controller_d); - let controller_d_cap = scenario.take_from_address(controller_d); + let mut controller_d_cap = scenario.take_from_address(controller_d); + let (token, borrow) = controller_d_cap.borrow(); - identity.did_doc().assert_is_member(&controller_d_cap); + identity.did_doc().assert_is_member(&token); + controller_d_cap.put_back(token, borrow); test_scenario::return_shared(identity); test_scenario::return_to_address(controller_a, controller_a_cap); @@ -536,6 +574,7 @@ module iota_identity::identity_tests { let identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 10, &clock, scenario.ctx(), @@ -544,12 +583,14 @@ module iota_identity::identity_tests { scenario.next_tx(controller_a); let mut identity = scenario.take_shared(); - let controller_b_cap = scenario.take_from_address(controller_b); + let mut controller_b_cap = scenario.take_from_address(controller_b); + let (token, borrow) = controller_b_cap.borrow(); - let proposal_id = identity.propose_new_controller(&controller_b_cap, option::none(), controller_d, 1, scenario.ctx()).destroy_some(); + let proposal_id = identity.propose_new_controller(&token, option::none(), controller_d, 1, scenario.ctx()).destroy_some(); scenario.next_tx(controller_b); - identity.execute_config_change(&controller_b_cap, proposal_id, scenario.ctx()); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller_b_cap.put_back(token, borrow); scenario.next_tx(controller_d); let controller_d_cap = scenario.take_from_address(controller_d); @@ -585,6 +626,7 @@ module iota_identity::identity_tests { let identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 10, &clock, scenario.ctx(), @@ -593,23 +635,28 @@ module iota_identity::identity_tests { scenario.next_tx(controller_b); let mut identity = scenario.take_shared(); - let controller_b_cap = scenario.take_from_address(controller_b); + let mut controller_b_cap = scenario.take_from_address(controller_b); + let (token, borrow) = controller_b_cap.borrow(); // Create a request to add a new controller. - let proposal_id = identity.propose_new_controller(&controller_b_cap, option::none(), controller_d, 10, scenario.ctx()).destroy_some(); + let proposal_id = identity.propose_new_controller(&token, option::none(), controller_d, 10, scenario.ctx()).destroy_some(); + controller_b_cap.put_back(token, borrow); scenario.next_tx(controller_b); - let controller_c_cap = scenario.take_from_address(controller_c); - identity.approve_proposal(&controller_c_cap, proposal_id); + let mut controller_c_cap = scenario.take_from_address(controller_c); + let (token, borrow) = controller_c_cap.borrow(); + identity.approve_proposal(&token, proposal_id); scenario.next_tx(controller_a); - identity.execute_config_change(&controller_c_cap, proposal_id, scenario.ctx()); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller_c_cap.put_back(token, borrow); scenario.next_tx(controller_d); - let controller_d_cap = scenario.take_from_address(controller_d); - - identity.did_doc().assert_is_member(&controller_d_cap); + let mut controller_d_cap = scenario.take_from_address(controller_d); + let (token, borrow) = controller_d_cap.borrow(); + identity.did_doc().assert_is_member(&token); + controller_d_cap.put_back(token, borrow); test_scenario::return_shared(identity); test_scenario::return_to_address(controller_b, controller_b_cap); @@ -639,6 +686,7 @@ module iota_identity::identity_tests { let second_identity = new_with_controllers( b"DID", controllers, + vec_map::empty(), 10, &clock, scenario.ctx(), @@ -647,18 +695,22 @@ module iota_identity::identity_tests { transfer::public_share_object(second_identity); scenario.next_tx(first_identity.to_address()); - let first_identity_cap = scenario.take_from_address(first_identity.to_address()); + let mut first_identity_cap = scenario.take_from_address(first_identity.to_address()); + let (token, borrow) = first_identity_cap.borrow(); let mut second_identity = scenario.take_shared(); assert!(second_identity.did_doc().controllers().contains(&first_identity_cap.id().to_inner()), 0); - second_identity.propose_new_controller(&first_identity_cap, option::none(), controller_a, 10, scenario.ctx()).destroy_none(); + second_identity.propose_new_controller(&token, option::none(), controller_a, 10, scenario.ctx()).destroy_none(); + first_identity_cap.put_back(token, borrow); scenario.next_tx(controller_a); - let controller_a_cap = scenario.take_from_address(controller_a); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); - second_identity.did_doc().assert_is_member(&controller_a_cap); + second_identity.did_doc().assert_is_member(&token); + controller_a_cap.put_back(token, borrow); test_scenario::return_shared(second_identity); test_scenario::return_to_address(controller_a, controller_a_cap); @@ -682,9 +734,11 @@ module iota_identity::identity_tests { // Propose a change for updating the did document let mut identity = scenario.take_shared(); - let cap = scenario.take_from_address(controller); + let mut cap = scenario.take_from_address(controller); + let (token, borrow) = cap.borrow(); - let _proposal_id = identity.propose_update(&cap, b"NOT DID", option::none(), &clock, scenario.ctx()); + let _proposal_id = identity.propose_update(&token, b"NOT DID", option::none(), &clock, scenario.ctx()); + cap.put_back(token, borrow); test_scenario::return_to_address(controller, cap); test_scenario::return_shared(identity); @@ -706,22 +760,28 @@ module iota_identity::identity_tests { controllers.insert(controller_a, 1); controllers.insert(controller_b, 1); - let identity = new_with_controllers(b"DID", controllers, 2, &clock, scenario.ctx()); + let identity = new_with_controllers(b"DID", controllers, vec_map::empty(), 2, &clock, scenario.ctx()); transfer::public_share_object(identity); scenario.next_tx(controller_a); let mut identity = scenario.take_shared(); - let cap = scenario.take_from_address(controller_a); - let proposal_id = identity.propose_new_controller(&cap, option::some(expiration_epoch), new_controller, 1, scenario.ctx()).destroy_some(); + let mut cap = scenario.take_from_address(controller_a); + let (token, borrow) = cap.borrow(); + let proposal_id = identity.propose_new_controller(&token, option::some(expiration_epoch), new_controller, 1, scenario.ctx()).destroy_some(); + cap.put_back(token, borrow); scenario.next_tx(controller_b); - let cap_b = scenario.take_from_address(controller_b); - identity.approve_proposal(&cap_b, proposal_id); + let mut cap_b = scenario.take_from_address(controller_b); + let (token, borrow) = cap_b.borrow(); + identity.approve_proposal(&token, proposal_id); + cap_b.put_back(token, borrow); scenario.later_epoch(100, controller_a); // this should fail! - identity.execute_config_change(&cap, proposal_id, scenario.ctx()); + let (token, borrow) = cap.borrow(); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + cap.put_back(token, borrow); test_scenario::return_to_address(controller_a, cap); test_scenario::return_to_address(controller_b, cap_b); diff --git a/identity_iota_core/packages/iota_identity/sources/migration.move b/identity_iota_core/packages/iota_identity/sources/migration.move index 566f41406..01b839ed7 100644 --- a/identity_iota_core/packages/iota_identity/sources/migration.move +++ b/identity_iota_core/packages/iota_identity/sources/migration.move @@ -73,7 +73,7 @@ module iota_identity::migration_tests { use iota_identity::migration::migrate_alias_output; use stardust::alias::{Self, Alias}; use iota_identity::migration_registry::{MigrationRegistry, init_testing}; - use iota_identity::multicontroller::ControllerCap; + use iota_identity::controller::ControllerCap; fun create_did_alias(ctx: &mut TxContext): Alias { let sender = ctx.sender(); @@ -115,13 +115,15 @@ module iota_identity::migration_tests { scenario.next_tx(controller_a); let identity = scenario.take_shared(); - let controller_a_cap = scenario.take_from_address(controller_a); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); - // Assert correct binding in migration regitry + // Assert correct binding in migration registry assert!(registry.lookup(alias_id) == identity.id().to_inner(), 0); // Assert the sender is controller - identity.did_doc().assert_is_member(&controller_a_cap); + identity.did_doc().assert_is_member(&token); + controller_a_cap.put_back(token, borrow); // assert the metadata is b"DID" let did = identity.did_doc().value(); diff --git a/identity_iota_core/packages/iota_identity/sources/multicontroller.move b/identity_iota_core/packages/iota_identity/sources/multicontroller.move index b8ae07f7e..3a49b5687 100644 --- a/identity_iota_core/packages/iota_identity/sources/multicontroller.move +++ b/identity_iota_core/packages/iota_identity/sources/multicontroller.move @@ -3,6 +3,8 @@ module iota_identity::multicontroller { use iota::{object_bag::{Self, ObjectBag}, vec_map::{Self, VecMap}, vec_set::{Self, VecSet}}; + use iota_identity::controller::{Self, DelegationToken, ControllerCap}; + use iota_identity::permissions; const EInvalidController: u64 = 0; const EControllerAlreadyVoted: u64 = 1; @@ -12,15 +14,6 @@ module iota_identity::multicontroller { const ENotVotedYet: u64 = 5; const EProposalNotFound: u64 = 6; - /// Capability that allows to access mutative APIs of a `Multicontroller`. - public struct ControllerCap has key { - id: UID, - } - - public fun id(self: &ControllerCap): &UID { - &self.id - } - /// Shares control of a value `V` with multiple entities called controllers. public struct Multicontroller has store { threshold: u64, @@ -28,33 +21,42 @@ module iota_identity::multicontroller { controlled_value: V, active_proposals: vector, proposals: ObjectBag, + revoked_tokens: VecSet, } /// Wraps a `V` in `Multicontroller`, making the tx's sender a controller with /// voting power 1. - public fun new(controlled_value: V, ctx: &mut TxContext): Multicontroller { - new_with_controller(controlled_value, ctx.sender(), ctx) + public fun new(controlled_value: V, can_delegate: bool, ctx: &mut TxContext): Multicontroller { + new_with_controller(controlled_value, ctx.sender(), can_delegate, ctx) } /// Wraps a `V` in `Multicontroller` and sends `controller` a `ControllerCap`. public fun new_with_controller( controlled_value: V, controller: address, + can_delegate: bool, ctx: &mut TxContext ): Multicontroller { let mut controllers = vec_map::empty(); controllers.insert(controller, 1); - new_with_controllers(controlled_value, controllers, 1, ctx) + if (can_delegate) { + new_with_controllers(controlled_value, vec_map::empty(), controllers, 1, ctx) + } else { + new_with_controllers(controlled_value, controllers, vec_map::empty(), 1, ctx) + } } /// Wraps a `V` in `Multicontroller`, settings `threshold` as the threshold, /// and using `controllers` to set controllers: i.e. each `(recipient, voting power)` /// in `controllers` results in `recipient` obtaining a `ControllerCap` with the /// specified voting power. + /// Controllers that are able to delegate their access, should be passed through + /// `controllers_that_can_delegate` parameter. public fun new_with_controllers( controlled_value: V, controllers: VecMap, + controllers_that_can_delegate: VecMap, threshold: u64, ctx: &mut TxContext, ): Multicontroller { @@ -64,10 +66,21 @@ module iota_identity::multicontroller { let addr = addrs.pop_back(); let vp = vps.pop_back(); - let cap = ControllerCap { id: object::new(ctx) }; - controllers.insert(cap.id.to_inner(), vp); + let cap = controller::new(false, ctx); + controllers.insert(cap.id().to_inner(), vp); + + cap.transfer(addr) + }; + + let (mut addrs, mut vps) = controllers_that_can_delegate.into_keys_values(); + while(!addrs.is_empty()) { + let addr = addrs.pop_back(); + let vp = vps.pop_back(); + + let cap = controller::new(true, ctx); + controllers.insert(cap.id().to_inner(), vp); - transfer::transfer(cap, addr); + cap.transfer(addr) }; let mut multi = Multicontroller { @@ -76,6 +89,7 @@ module iota_identity::multicontroller { threshold, active_proposals: vector[], proposals: object_bag::new(ctx), + revoked_tokens: vec_set::empty(), }; multi.set_threshold(threshold); @@ -102,7 +116,7 @@ module iota_identity::multicontroller { } } - /// Strucure that encapsulate the kind of change that will be performed + /// Structure that encapsulate the kind of change that will be performed /// when a proposal is carried out. public struct Action { inner: T, @@ -124,26 +138,28 @@ module iota_identity::multicontroller { &mut action.inner } - public(package) fun assert_is_member(multi: &Multicontroller, cap: &ControllerCap) { - assert!(multi.controllers.contains(&cap.id.to_inner()), EInvalidController); + public(package) fun assert_is_member(multi: &Multicontroller, cap: &DelegationToken) { + assert!(multi.controllers.contains(&cap.controller()), EInvalidController); } /// Creates a new proposal for `Multicontroller` `multi`. public fun create_proposal( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, action: T, expiration_epoch: Option, ctx: &mut TxContext, ): ID { multi.assert_is_member(cap); - let cap_id = cap.id.to_inner(); + cap.assert_has_permission(permissions::can_create_proposal()); + + let cap_id = cap.controller(); let voting_power = multi.voting_power(cap_id); let proposal = Proposal { id: object::new(ctx), votes: voting_power, - voters: vec_set::singleton(cap.id.to_inner()), + voters: vec_set::singleton(cap_id), expiration_epoch, action, }; @@ -157,11 +173,13 @@ module iota_identity::multicontroller { /// Approves an active `Proposal` in `multi`. public fun approve_proposal( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ) { multi.assert_is_member(cap); - let cap_id = cap.id.to_inner(); + cap.assert_has_permission(permissions::can_approve_proposal()); + + let cap_id = cap.controller(); let voting_power = multi.voting_power(cap_id); let proposal = multi.proposals.borrow_mut>(proposal_id); @@ -176,11 +194,12 @@ module iota_identity::multicontroller { /// This call fails if `multi`'s threshold has not been reached. public fun execute_proposal( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ): Action { multi.assert_is_member(cap); + cap.assert_has_permission(permissions::can_execute_proposal()); let proposal = multi.proposals.remove>(proposal_id); assert!(proposal.votes >= multi.threshold, EThresholdNotReached); @@ -208,10 +227,12 @@ module iota_identity::multicontroller { /// `proposal_id`. public fun remove_approval( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ) { - let cap_id = cap.id.to_inner(); + cap.assert_has_permission(permissions::can_remove_approval()); + + let cap_id = cap.controller(); let vp = multi.voting_power(cap_id); let proposal = multi.proposals.borrow_mut>(proposal_id); @@ -257,6 +278,37 @@ module iota_identity::multicontroller { sum } + /// Revoke the `DelegationToken` with `ID` `deny_id`. Only controllers can perform this operation. + public fun revoke_token(self: &mut Multicontroller, cap: &ControllerCap, deny_id: ID) { + assert!(self.controllers.contains(object::borrow_id(cap)), EInvalidController); + self.revoked_tokens.insert(deny_id); + } + + /// Un-revoke a `DelegationToken`. + public fun unrevoke_token(self: &mut Multicontroller, cap: &ControllerCap, token_id: ID) { + assert!(self.controllers.contains(object::borrow_id(cap)), EInvalidController); + self.revoked_tokens.remove(&token_id); + } + + /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from + /// the controller committee. + public fun destroy_controller_cap(self: &Multicontroller, cap: ControllerCap) { + assert!(self.controllers.contains(&cap.id().to_inner()), EInvalidController); + + cap.delete(); + } + + /// Destroys a `DelegationToken`. + public fun destroy_delegation_token(self: &mut Multicontroller, token: DelegationToken) { + let token_id = object::id(&token); + let is_revoked = self.revoked_tokens.contains(&token_id); + if (is_revoked) { + self.revoked_tokens.remove(&token_id); + }; + + token.delete(); + } + public(package) fun unpack_action(action: Action): T { let Action { inner } = action; inner @@ -271,9 +323,9 @@ module iota_identity::multicontroller { let mut i = 0; while (i < to_add.size()) { let (addr, vp) = to_add.get_entry_by_idx(i); - let new_cap = ControllerCap { id: object::new(ctx) }; - multi.controllers.insert(new_cap.id.to_inner(), *vp); - transfer::transfer(new_cap, *addr); + let new_cap = controller::new(false, ctx); + multi.controllers.insert(new_cap.id().to_inner(), *vp); + new_cap.transfer(*addr); i = i + 1; } } diff --git a/identity_iota_core/packages/iota_identity/sources/permissions.move b/identity_iota_core/packages/iota_identity/sources/permissions.move new file mode 100644 index 000000000..152e7782a --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/permissions.move @@ -0,0 +1,20 @@ +module iota_identity::permissions { + /// Permission that enables a controller's delegate to create proposals. + const CAN_CREATE_PROPOSAL: u32 = 0x1; + /// Permission that enables a controller's delegate to approve proposals. + const CAN_APPROVE_PROPOSAL: u32 = 0x1 << 1; + /// Permission that enables a controller's delegate to execute proposals. + const CAN_EXECUTE_PROPOSAL: u32 = 0x1 << 2; + /// Permission that enables a controller's delegate to delete proposals. + const CAN_DELETE_PROPOSAL: u32 = 0x1 << 3; + /// Permission that enables a controller's delegate to remove a proposal's approval. + const CAN_REMOVE_APPROVAL: u32 = 0x1 << 4; + const ALL_PERMISSIONS: u32 = 0xFFFFFFFF; + + public fun can_create_proposal(): u32 { CAN_CREATE_PROPOSAL } + public fun can_approve_proposal(): u32 { CAN_APPROVE_PROPOSAL } + public fun can_execute_proposal(): u32 { CAN_EXECUTE_PROPOSAL } + public fun can_delete_proposal(): u32 { CAN_DELETE_PROPOSAL } + public fun can_remove_approval(): u32 { CAN_REMOVE_APPROVAL } + public fun all(): u32 { ALL_PERMISSIONS } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move index 4195e3461..dca212e8c 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module iota_identity::borrow_proposal { - use iota_identity::{multicontroller::{Multicontroller, Action, ControllerCap}}; + use iota_identity::multicontroller::{Multicontroller, Action}; + use iota_identity::controller::DelegationToken; use iota::transfer::Receiving; const EInvalidObject: u64 = 0; @@ -19,7 +20,7 @@ module iota_identity::borrow_proposal { /// Propose the borrowing of a set of assets owned by this multicontroller. public fun propose_borrow( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, objects: vector, owner: address, diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/config.move b/identity_iota_core/packages/iota_identity/sources/proposals/config.move index 80755b2af..2758373a8 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/config.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/config.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module iota_identity::config_proposal { - use iota_identity::multicontroller::{ControllerCap, Multicontroller}; + use iota_identity::multicontroller::Multicontroller; + use iota_identity::controller::DelegationToken; use iota::vec_map::VecMap; const ENotMember: u64 = 0; @@ -17,7 +18,7 @@ module iota_identity::config_proposal { public fun propose_modify( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, mut threshold: Option, controllers_to_add: VecMap, @@ -87,7 +88,7 @@ module iota_identity::config_proposal { public fun execute_modify( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ) { diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move index d16f8c36c..cd0a82440 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module iota_identity::transfer_proposal { - use iota_identity::{multicontroller::{Multicontroller, Action, ControllerCap}}; + use iota_identity::multicontroller::{Multicontroller, Action}; + use iota_identity::controller::DelegationToken; use iota::transfer::Receiving; const EDifferentLength: u64 = 0; @@ -16,7 +17,7 @@ module iota_identity::transfer_proposal { public fun propose_send( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, expiration: Option, objects: vector, recipients: vector
, diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/value.move b/identity_iota_core/packages/iota_identity/sources/proposals/value.move index 468971af6..1788ed55d 100644 --- a/identity_iota_core/packages/iota_identity/sources/proposals/value.move +++ b/identity_iota_core/packages/iota_identity/sources/proposals/value.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 module iota_identity::update_value_proposal { - use iota_identity::multicontroller::{Multicontroller, ControllerCap}; + use iota_identity::multicontroller::Multicontroller; + use iota_identity::controller::DelegationToken; public struct UpdateValue has store { new_value: V, @@ -10,7 +11,7 @@ module iota_identity::update_value_proposal { public fun propose_update( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, new_value: V, expiration: Option, ctx: &mut TxContext, @@ -21,10 +22,12 @@ module iota_identity::update_value_proposal { public fun execute_update( multi: &mut Multicontroller, - cap: &ControllerCap, + cap: &DelegationToken, proposal_id: ID, ctx: &mut TxContext, ) { + + let action = multi.execute_proposal(cap, proposal_id, ctx); let UpdateValue { new_value } = action.unpack_action(); diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs index 4b7fa47f8..938c03ded 100644 --- a/identity_iota_core/src/rebased/migration/identity.rs +++ b/identity_iota_core/src/rebased/migration/identity.rs @@ -144,7 +144,7 @@ impl OnChainIdentity { } pub(crate) async fn get_controller_cap(&self, client: &IdentityClient) -> Result { - let controller_cap_tag = StructTag::from_str(&format!("{}::multicontroller::ControllerCap", client.package_id())) + let controller_cap_tag = StructTag::from_str(&format!("{}::controller::ControllerCap", client.package_id())) .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; client .find_owned_ref(controller_cap_tag, |obj_data| { @@ -170,7 +170,7 @@ impl OnChainIdentity { } /// Sends assets owned by this [`OnChainIdentity`] to other addresses. - pub fn send_assets(&mut self) -> ProposalBuilder { + pub fn send_assets(&mut self) -> ProposalBuilder<'_, SendAction> { ProposalBuilder::new(self, SendAction::default()) } @@ -178,7 +178,7 @@ impl OnChainIdentity { /// # Notes /// Make sure to call [`super::Proposal::with_intent`] before executing the proposal. /// Failing to do so will make [`crate::proposals::ProposalT::execute`] return an error. - pub fn borrow_assets(&mut self) -> ProposalBuilder { + pub fn borrow_assets(&mut self) -> ProposalBuilder<'_, BorrowAction> { ProposalBuilder::new(self, BorrowAction::default()) } diff --git a/identity_iota_core/src/rebased/proposals/borrow.rs b/identity_iota_core/src/rebased/proposals/borrow.rs index 60d309e7b..d14292e2b 100644 --- a/identity_iota_core/src/rebased/proposals/borrow.rs +++ b/identity_iota_core/src/rebased/proposals/borrow.rs @@ -36,8 +36,6 @@ pub(crate) type IntentFn = Box, - #[serde(skip)] - intent: Option, } /// A [`BorrowAction`] coupled with a user-provided function to describe how @@ -89,17 +87,6 @@ impl<'i> ProposalBuilder<'i, BorrowAction> { } } -impl Proposal { - /// Defines how the borrowed assets should be used. - pub fn with_intent(mut self, intent_fn: F) -> Self - where - F: FnOnce(&mut Ptb, &HashMap) + Send + 'static, - { - self.action.intent = Some(Box::new(intent_fn)); - self - } -} - #[async_trait] impl ProposalT for Proposal { type Action = BorrowAction; diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs index 2e5268e5e..95837f3ec 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs @@ -27,6 +27,7 @@ pub(crate) fn propose_borrow( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; let objects_arg = ptb.pure(objects)?; @@ -36,9 +37,11 @@ pub(crate) fn propose_borrow( ident_str!("identity").into(), ident_str!("propose_borrow").into(), vec![], - vec![identity_arg, cap_arg, exp_arg, objects_arg], + vec![identity_arg, delegation_token, exp_arg, objects_arg], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } @@ -56,6 +59,7 @@ where let mut ptb = ProgrammableTransactionBuilder::new(); let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow)= utils::get_controller_delegation(&mut ptb, controller_cap, package); let proposal_id = ptb.pure(proposal_id)?; // Get the proposal's action as argument. @@ -64,9 +68,11 @@ where ident_str!("identity").into(), ident_str!("execute_proposal").into(), vec![BorrowAction::move_type(package)], - vec![identity, controller_cap, proposal_id], + vec![identity, delegation_token, proposal_id], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + // Borrow all the objects specified in the action. let obj_arg_map = objects .into_iter() diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs index 26a4d8d94..60fb207c5 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs @@ -8,7 +8,6 @@ use iota_sdk::rpc_types::OwnedObjectRef; use iota_sdk::types::base_types::IotaAddress; use iota_sdk::types::base_types::ObjectID; use iota_sdk::types::base_types::ObjectRef; -use iota_sdk::types::object::Owner; use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; use iota_sdk::types::transaction::ObjectArg; use iota_sdk::types::transaction::ProgrammableTransaction; @@ -62,6 +61,7 @@ where }; let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(controller_cap))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); let expiration = utils::option_to_move(expiration, &mut ptb, package)?; let threshold = utils::option_to_move(threshold, &mut ptb, package)?; let controllers_to_remove = ptb.pure(controllers_to_remove)?; @@ -73,7 +73,7 @@ where vec![], vec![ identity, - controller_cap, + delegation_token, expiration, threshold, controllers_to_add, @@ -82,6 +82,8 @@ where ], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + Ok(ptb.finish()) } @@ -93,23 +95,19 @@ pub(crate) fn execute_config_change( ) -> anyhow::Result { let mut ptb = ProgrammableTransactionBuilder::new(); - let Owner::Shared { initial_shared_version } = identity.owner else { - anyhow::bail!("identity \"{}\" is a not shared object", identity.reference.object_id); - }; - let identity = ptb.obj(ObjectArg::SharedObject { - id: identity.reference.object_id, - initial_shared_version, - mutable: true, - })?; + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(controller_cap))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); let proposal_id = ptb.pure(proposal_id)?; ptb.programmable_move_call( package, ident_str!("identity").into(), ident_str!("execute_config_change").into(), vec![], - vec![identity, controller_cap, proposal_id], + vec![identity, delegation_token, proposal_id], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs index 32be404e0..f36ce0b83 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs @@ -67,6 +67,14 @@ where vec![ids, vps], ) }; + + let controllers_that_can_delegate = ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("vec_map").into(), + ident_str!("empty").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![], + ); let doc_arg = ptb.pure(did_doc).map_err(|e| Error::InvalidArgument(e.to_string()))?; let threshold_arg = ptb.pure(threshold).map_err(|e| Error::InvalidArgument(e.to_string()))?; let clock = utils::get_clock_ref(&mut ptb); @@ -77,7 +85,7 @@ where module: ident_str!("identity").into(), function: ident_str!("new_with_controllers").into(), type_arguments: vec![], - arguments: vec![doc_arg, controllers, threshold_arg, clock], + arguments: vec![doc_arg, controllers, controllers_that_can_delegate, threshold_arg, clock], }))); // Share the resulting identity. diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs index bb1c7767f..b1cb785e3 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs @@ -19,6 +19,7 @@ pub(crate) fn propose_deactivation( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; let clock = utils::get_clock_ref(&mut ptb); @@ -28,9 +29,11 @@ pub(crate) fn propose_deactivation( ident_str!("identity").into(), ident_str!("propose_deactivation").into(), vec![], - vec![identity_arg, cap_arg, exp_arg, clock], + vec![identity_arg, delegation_token, exp_arg, clock], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } @@ -42,6 +45,7 @@ pub(crate) fn execute_deactivation( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let proposal_id = ptb.pure(proposal_id)?; let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let clock = utils::get_clock_ref(&mut ptb); @@ -51,8 +55,10 @@ pub(crate) fn execute_deactivation( ident_str!("identity").into(), ident_str!("execute_deactivation").into(), vec![], - vec![identity_arg, cap_arg, proposal_id, clock], + vec![identity_arg, delegation_token, proposal_id, clock], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs index 4335bc194..bb04d73a1 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs @@ -1,12 +1,12 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use crate::rebased::sui::move_calls::utils; use crate::rebased::utils::MoveType; use crate::rebased::Error; use iota_sdk::rpc_types::OwnedObjectRef; use iota_sdk::types::base_types::ObjectID; use iota_sdk::types::base_types::ObjectRef; -use iota_sdk::types::object::Owner; use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; use iota_sdk::types::transaction::ObjectArg; use iota_sdk::types::transaction::ProgrammableTransaction; @@ -19,22 +19,12 @@ pub(crate) fn approve( package: ObjectID, ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); - let Owner::Shared { initial_shared_version } = identity.owner else { - return Err(Error::TransactionBuildingFailed(format!( - "Identity \"{}\" is not a shared object", - identity.object_id() - ))); - }; - let identity = ptb - .obj(ObjectArg::SharedObject { - id: identity.object_id(), - initial_shared_version, - mutable: true, - }) - .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; let controller_cap = ptb .obj(ObjectArg::ImmOrOwnedObject(controller_cap)) .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); let proposal_id = ptb .pure(proposal_id) .map_err(|e| Error::InvalidArgument(e.to_string()))?; @@ -44,8 +34,10 @@ pub(crate) fn approve( ident_str!("identity").into(), ident_str!("approve_proposal").into(), vec![T::move_type(package)], - vec![identity, controller_cap, proposal_id], + vec![identity, delegation_token, proposal_id], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs index 19c1a9f75..83348a867 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs @@ -15,6 +15,8 @@ use crate::rebased::proposals::SendAction; use crate::rebased::sui::move_calls; use crate::rebased::utils::MoveType; +use self::move_calls::utils; + pub(crate) fn propose_send( identity: OwnedObjectRef, capability: ObjectRef, @@ -24,6 +26,7 @@ pub(crate) fn propose_send( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let identity_arg = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let exp_arg = move_calls::utils::option_to_move(expiration, &mut ptb, package_id)?; let (objects, recipients) = { @@ -39,9 +42,11 @@ pub(crate) fn propose_send( ident_str!("identity").into(), ident_str!("propose_send").into(), vec![], - vec![identity_arg, cap_arg, exp_arg, objects, recipients], + vec![identity_arg, delegation_token, exp_arg, objects, recipients], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } @@ -55,6 +60,7 @@ pub(crate) fn execute_send( let mut ptb = ProgrammableTransactionBuilder::new(); let identity = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, controller_cap, package); let proposal_id = ptb.pure(proposal_id)?; // Get the proposal's action as argument. @@ -63,9 +69,11 @@ pub(crate) fn execute_send( ident_str!("identity").into(), ident_str!("execute_proposal").into(), vec![SendAction::move_type(package)], - vec![identity, controller_cap, proposal_id], + vec![identity, delegation_token, proposal_id], ); + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + // Send each object in this send action. // Traversing the map in reverse reduces the number of operations on the move side. for (obj, obj_type) in objects.into_iter().rev() { diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs index 93603b1da..9ec751425 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs @@ -20,6 +20,7 @@ pub(crate) fn propose_update( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token , borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; let doc_arg = ptb.pure(did_doc.as_ref())?; @@ -30,9 +31,11 @@ pub(crate) fn propose_update( ident_str!("identity").into(), ident_str!("propose_update").into(), vec![], - vec![identity_arg, cap_arg, doc_arg, exp_arg, clock], + vec![identity_arg, delegation_token, doc_arg, exp_arg, clock], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } @@ -44,6 +47,7 @@ pub(crate) fn execute_update( ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow)= utils::get_controller_delegation(&mut ptb, cap_arg, package_id); let proposal_id = ptb.pure(proposal_id)?; let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; let clock = utils::get_clock_ref(&mut ptb); @@ -53,8 +57,10 @@ pub(crate) fn execute_update( ident_str!("identity").into(), ident_str!("execute_update").into(), vec![], - vec![identity_arg, cap_arg, proposal_id, clock], + vec![identity_arg, delegation_token, proposal_id, clock], ); + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + Ok(ptb.finish()) } diff --git a/identity_iota_core/src/rebased/sui/move_calls/utils.rs b/identity_iota_core/src/rebased/sui/move_calls/utils.rs index c35ee6552..bb8b66525 100644 --- a/identity_iota_core/src/rebased/sui/move_calls/utils.rs +++ b/identity_iota_core/src/rebased/sui/move_calls/utils.rs @@ -27,6 +27,36 @@ pub(crate) fn get_clock_ref(ptb: &mut Ptb) -> Argument { .expect("network has a singleton clock instantiated") } +pub(crate) fn get_controller_delegation(ptb: &mut Ptb, controller_cap: Argument, package: ObjectID) -> (Argument, Argument) { + let Argument::Result(idx) = ptb.programmable_move_call( + package, + ident_str!("controller").into(), + ident_str!("borrow").into(), + vec![], + vec![controller_cap], + ) else { + unreachable!("making move calls always return a result variant"); + }; + + (Argument::NestedResult(idx, 0), Argument::NestedResult(idx, 1)) +} + +pub(crate) fn put_back_delegation_token( + ptb: &mut Ptb, + controller_cap: Argument, + delegation_token: Argument, + borrow: Argument, + package: ObjectID, +) { + ptb.programmable_move_call( + package, + ident_str!("controller").into(), + ident_str!("put_back").into(), + vec![], + vec![controller_cap, delegation_token, borrow] + ); +} + pub(crate) fn owned_ref_to_shared_object_arg( owned_ref: OwnedObjectRef, ptb: &mut Ptb, diff --git a/identity_iota_core/tests/e2e/identity.rs b/identity_iota_core/tests/e2e/identity.rs index 7075128d2..2bd6236ad 100644 --- a/identity_iota_core/tests/e2e/identity.rs +++ b/identity_iota_core/tests/e2e/identity.rs @@ -158,7 +158,7 @@ async fn adding_controller_works() -> anyhow::Result<()> { let cap = bob_client .find_owned_ref( - StructTag::from_str(&format!("{}::multicontroller::ControllerCap", test_client.package_id())).unwrap(), + StructTag::from_str(&format!("{}::controller::ControllerCap", test_client.package_id())).unwrap(), |_| true, ) .await?;