From 46087b5006a0995539ce9e1b2803a53442a9e620 Mon Sep 17 00:00:00 2001 From: 0o-de-lally <1364012+0o-de-lally@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:54:11 -0400 Subject: [PATCH 01/68] [testsuites] isolate Move policy upgrade testing, make backwards compat test optional (#286) --- .github/workflows/ci.yaml | 13 +++++++++---- upgrade-tests/tests/force_upgrade_mainnet.rs | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6427680a6..680234bc4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ on: - "release**" - "main**" schedule: - - cron: '30 00 * * *' + - cron: "30 00 * * *" env: DIEM_FORGE_NODE_BIN_PATH: ${{github.workspace}}/diem-node @@ -212,14 +212,19 @@ jobs: if: always() working-directory: ./upgrade-tests # NOTE: upgrade tests which compile Move code, and then submit in the same thread will cause a stack overflow with the default rust stack size. - run: RUST_MIN_STACK=104857600 cargo test --no-fail-fast -- --skip compatible_ + run: RUST_MIN_STACK=104857600 cargo test --no-fail-fast check_workflow + - name: upgrade - can force + if: always() + working-directory: ./upgrade-tests + # NOTE: upgrade tests which compile Move code, and then submit in the same thread will cause a stack overflow with the default rust stack size. + run: RUST_MIN_STACK=104857600 cargo test --no-fail-fast force_upgrade - name: upgrade - should be backwards compatible # should always run unless we explicitly mark the branch or tag as "breaking" - if: ${{ ! contains(github.ref_name, 'breaking/') }} + if: not(contains(${{github.ref_name}}, "breaking/")) working-directory: ./upgrade-tests # NOTE: upgrade tests which compile Move code, and then submit in the same thread will cause a stack overflow with the default rust stack size. - run: RUST_MIN_STACK=104857600 cargo test compatible_ + run: RUST_MIN_STACK=104857600 cargo test --no-fail-fast backwards_compatible rescue: timeout-minutes: 60 needs: [build-framework] diff --git a/upgrade-tests/tests/force_upgrade_mainnet.rs b/upgrade-tests/tests/force_upgrade_mainnet.rs index a8756804f..a882c63e2 100644 --- a/upgrade-tests/tests/force_upgrade_mainnet.rs +++ b/upgrade-tests/tests/force_upgrade_mainnet.rs @@ -8,7 +8,7 @@ use libra_framework::release::ReleaseTarget; // /// Force upgrade Libra #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn force_upgrade_mainnet_libra() { +async fn smoke_upgrade_mainnet_force_libra() { support::upgrade_multiple_impl( "upgrade-single-lib-force", vec!["1-libra-framework"], @@ -20,7 +20,7 @@ async fn force_upgrade_mainnet_libra() { /// Upgrade all modules #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn force_upgrade_mainnet_multiple() { +async fn smoke_upgrade_mainnet_force_multiple() { support::upgrade_multiple_impl( "upgrade-multi-lib-force", vec!["1-move-stdlib", "2-vendor-stdlib", "3-libra-framework"], From 3dbf698123a9c6ca6951903e27ce63164b81c098 Mon Sep 17 00:00:00 2001 From: 0o-de-lally <1364012+0o-de-lally@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:16:52 -0400 Subject: [PATCH 02/68] [testsuite] isolating CI Move upgrade testing (#289) --- .github/workflows/ci.yaml | 9 ++------- upgrade-tests/tests/force_upgrade_mainnet.rs | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 680234bc4..96f28848f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -212,19 +212,14 @@ jobs: if: always() working-directory: ./upgrade-tests # NOTE: upgrade tests which compile Move code, and then submit in the same thread will cause a stack overflow with the default rust stack size. - run: RUST_MIN_STACK=104857600 cargo test --no-fail-fast check_workflow + run: RUST_MIN_STACK=104857600 cargo test --no-fail-fast -- --skip compatible_ - - name: upgrade - can force - if: always() - working-directory: ./upgrade-tests - # NOTE: upgrade tests which compile Move code, and then submit in the same thread will cause a stack overflow with the default rust stack size. - run: RUST_MIN_STACK=104857600 cargo test --no-fail-fast force_upgrade - name: upgrade - should be backwards compatible # should always run unless we explicitly mark the branch or tag as "breaking" if: not(contains(${{github.ref_name}}, "breaking/")) working-directory: ./upgrade-tests # NOTE: upgrade tests which compile Move code, and then submit in the same thread will cause a stack overflow with the default rust stack size. - run: RUST_MIN_STACK=104857600 cargo test --no-fail-fast backwards_compatible + run: RUST_MIN_STACK=104857600 cargo test compatible_ rescue: timeout-minutes: 60 needs: [build-framework] diff --git a/upgrade-tests/tests/force_upgrade_mainnet.rs b/upgrade-tests/tests/force_upgrade_mainnet.rs index a882c63e2..a8756804f 100644 --- a/upgrade-tests/tests/force_upgrade_mainnet.rs +++ b/upgrade-tests/tests/force_upgrade_mainnet.rs @@ -8,7 +8,7 @@ use libra_framework::release::ReleaseTarget; // /// Force upgrade Libra #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn smoke_upgrade_mainnet_force_libra() { +async fn force_upgrade_mainnet_libra() { support::upgrade_multiple_impl( "upgrade-single-lib-force", vec!["1-libra-framework"], @@ -20,7 +20,7 @@ async fn smoke_upgrade_mainnet_force_libra() { /// Upgrade all modules #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn smoke_upgrade_mainnet_force_multiple() { +async fn force_upgrade_mainnet_multiple() { support::upgrade_multiple_impl( "upgrade-multi-lib-force", vec!["1-move-stdlib", "2-vendor-stdlib", "3-libra-framework"], From 9ba97a083860b15c418b9796c606a930301f5e2f Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:27:08 -0300 Subject: [PATCH 03/68] feat multi action offer - WIP --- .../ol_sources/community_wallet_init.move | 2 +- .../tests/vote_lib/multi_action.test.move | 246 +++++++++++++++--- .../ol_sources/vote_lib/multi_action.move | 140 +++++++++- 3 files changed, 347 insertions(+), 41 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/community_wallet_init.move b/framework/libra-framework/sources/ol_sources/community_wallet_init.move index fc0be3358..464a13308 100644 --- a/framework/libra-framework/sources/ol_sources/community_wallet_init.move +++ b/framework/libra-framework/sources/ol_sources/community_wallet_init.move @@ -61,7 +61,7 @@ module ol_framework::community_wallet_init { //////// MULTISIG TX HELPERS //////// // Helper to initialize the PaymentMultiAction but also while confirming that the signers are not related family - // These transactions can be sent directly to donor_voice, but this is a helper to make it easier to initialize the multisig with the acestry requirements. + // These transactions can be sent directly to donor_voice, but this is a helper to make it easier to initialize the multisig with the ancestry requirements. public entry fun init_community( sig: &signer, diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index e4941d894..d44ca25e1 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -1,4 +1,3 @@ - #[test_only] module ol_framework::test_multi_action { use ol_framework::mock; @@ -11,35 +10,214 @@ module ol_framework::test_multi_action { use ol_framework::ol_account; use diem_framework::resource_account; use diem_framework::reconfiguration; + use diem_framework::account; - // use diem_std::debug::print; - - struct DummyType has drop, store {} - - #[test(root = @ol_framework, dave = @0x1000d)] - fun init_multi_action(root: &signer, dave: &signer) { + use diem_std::debug::print; + struct DummyType has drop, store {} + #[test(root = @ol_framework, carol = @0x1000c)] + fun init_multi_action(root: &signer, carol: &signer) { mock::genesis_n_vals(root, 2); - - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 0); // make the vals the signers on the safe multi_action::init_gov(&resource_sig); multi_action::init_type(&resource_sig, true); + } + + // Happy Day: propose offer to authorities + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun propose_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let carol_address = @0x1000c; + + // check the offer does not exist + assert!(!multi_action::exists_offer(carol_address), 0); + assert!(!multi_action::is_multi_action(carol_address), 0); + + // initialize the multi_action account + multi_action::init_gov(carol); + + // offer authorities + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::some(3)); + + // check the offer is proposed and account is not muti_action yet + assert!(multi_action::exists_offer(carol_address), 0); + assert!(multi_action::get_offer_proposed(carol_address) == authorities, 0); + assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 0); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 3, 0); + assert!(!multi_action::is_multi_action(carol_address), 0); + } + + // Happy Day: claim offer by authorities + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun claim_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let carol_address = @0x1000c; + + // initialize the multi_action account + multi_action::init_gov(carol); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + + // bob claim the offer + multi_action::claim_offer(bob, carol_address); + + // check the claimed offer + assert!(multi_action::exists_offer(carol_address), 0); + let claimed = vector::singleton(signer::address_of(bob)); + let proposed = vector::singleton(signer::address_of(alice)); + assert!(multi_action::get_offer_claimed(carol_address) == claimed, 0); + assert!(multi_action::get_offer_proposed(carol_address) == proposed, 0); + + // alice claim the offer + multi_action::claim_offer(alice, carol_address); + + // check alice and bob claimed the offer + let claimed = multi_action::get_offer_claimed(carol_address); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(alice)); + assert!(claimed == authorities, 0); + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 0); + } + + // Happy Day: finalize multisign account + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun finalize_multi_action(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + + // initialize the multi_action account + multi_action::init_gov(carol); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + + // authorities claim the offer + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + + // finalize the multi_action account + assert!(account::exists_at(carol_address), 666); + multi_action::finalize_and_cage2(carol); + + // check the account is multi_action + assert!(multi_action::is_multi_action(carol_address), 0); + + // check authorities + let authorities = multi_action::get_authorities(carol_address); + let claimed = vector::empty
(); + vector::push_back(&mut claimed, signer::address_of(alice)); + vector::push_back(&mut claimed, signer::address_of(bob)); + print(&authorities); + assert!(authorities == claimed, 0); + + // check offer was removed + assert!(!multi_action::exists_offer(carol_address), 0); + } + + // Try to claim expired offer + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 15, location = ol_framework::multi_action)] + fun claim_expired_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); + let new_resource_address = signer::address_of(&resource_sig); + // initialize the multi_action account + multi_action::init_gov(&resource_sig); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(&resource_sig, authorities, option::some(2)); + + // alice claim the offer + multi_action::claim_offer(alice, new_resource_address); + + mock::trigger_epoch(root); // epoch 1 valid + mock::trigger_epoch(root); // epoch 2 valid + mock::trigger_epoch(root); // epoch 3 expired + + // bob claim expired offer + multi_action::claim_offer(bob, new_resource_address); + } + + #[test] + fun test_adal() { + let authorities: vector
= vector::empty(); + let alice = @0x1000a; + let bob = @0x1000b; + vector::push_back(&mut authorities, alice); + vector::push_back(&mut authorities, bob); + assert!(vector::length(&authorities) == 2, 0); + assert!(*vector::borrow(&authorities, 0) == alice, 0); + print(&authorities); + + let x = 0; + let y = 1; + let r = if (false) { + &mut x + } else { + &mut y + }; + *r = *r + 1; + assert!(*r == 2, 0); + print(r); + + let text = x"ADA1"; + print(&text); + + let a = 0; + let b = copy a + 1; + let c = a + 2; + + print(&b); + print(&c); } - #[test(root = @ol_framework, dave = @0x1000d, alice = @0x1000a)] - fun propose_action(root: &signer, dave: &signer, alice: &signer) { + /* + #[test(carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun offer_authorities(carol: &signer, alice: &signer, bob: &signer) { + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); + let new_resource_address = signer::address_of(&resource_sig); + assert!(resource_account::is_resource_account(new_resource_address), 0); + + print(&1111111); + + // offer authorities to resource_sig for alice and bob + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + // multi_action::propose_offer(&resource_sig, authorities, 7); + + //print(&resource_sig); + + }*/ + + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a)] + fun propose_action(root: &signer, carol: &signer, alice: &signer) { let vals = mock::genesis_n_vals(root, 2); // mock::ol_initialize_coin(root); - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 0); @@ -47,6 +225,7 @@ module ol_framework::test_multi_action { // SO ALICE IS AUTHORIZED multi_action::init_gov(&resource_sig); multi_action::init_type(&resource_sig, true); + //need to be caged to finalize multi action workflow and release control of the account multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); @@ -57,16 +236,15 @@ module ol_framework::test_multi_action { // SHOULD NOT HAVE COUNTED ANY VOTES let v = multi_action::get_votes(new_resource_address, guid::id_creation_num(&id)); assert!(vector::length(&v) == 0, 7357003); - } - #[test(root = @ol_framework, dave = @0x1000d, alice = @0x1000a, bob = @0x1000a)] - fun propose_action_prevent_duplicated(root: &signer, dave: &signer, alice: &signer, bob: &signer) { + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000a)] + fun propose_action_prevent_duplicated(root: &signer, carol: &signer, alice: &signer, bob: &signer) { // Scenario: alice and bob are authorities. They try to send the same proposal let vals = mock::genesis_n_vals(root, 2); // mock::ol_initialize_coin(root); - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 0); @@ -108,14 +286,14 @@ module ol_framework::test_multi_action { assert!(epoch == 14, 7357005); } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d)] - fun vote_action_happy_simple(root: &signer, alice: &signer, bob: &signer, dave: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun vote_action_happy_simple(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // Scenario: a simple MultiAction where we don't need any capabilities. Only need to know if the result was successful on the vote that crossed the threshold. let vals = mock::genesis_n_vals(root, 2); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 0); @@ -148,14 +326,14 @@ module ol_framework::test_multi_action { } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d)] - fun vote_action_happy_withdraw_cap(root: &signer, alice: &signer, bob: &signer, dave: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun vote_action_happy_withdraw_cap(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // Scenario: testing that a payment type multisig could be created with this module: that the WithdrawCapability can be used here. let vals = mock::genesis_n_vals(root, 2); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 0); @@ -189,11 +367,11 @@ module ol_framework::test_multi_action { &cap, 42, ); - ol_account::create_account(root, @0x1000d); - ol_account::deposit_coins(@0x1000d, c); + ol_account::create_account(root, @0x1000c); + ol_account::deposit_coins(@0x1000c, c); option::fill(&mut cap_opt, cap); - let (_, balance) = ol_account::balance(@0x1000d); + let (_, balance) = ol_account::balance(@0x1000c); assert!(balance == 42, 7357004); @@ -201,10 +379,10 @@ module ol_framework::test_multi_action { } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d)] + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 65548, location = ol_framework::multi_action)] - fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, dave: &signer) { + fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // Scenario: Testing that if an action expires voting cannot be done. let vals = mock::genesis_n_vals(root, 2); @@ -213,7 +391,7 @@ module ol_framework::test_multi_action { let epoch = reconfiguration::get_current_epoch(); assert!(epoch == 0, 7357001); // Dave creates the resource account. He is not one of the validators, and is not an authority in the multisig. - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 7357002); @@ -244,8 +422,8 @@ module ol_framework::test_multi_action { } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d, marlon_rando = @0x123456)] - fun governance_change_auths(root: &signer, alice: &signer, bob: &signer, dave: &signer, marlon_rando: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, marlon_rando = @0x123456)] + fun governance_change_auths(root: &signer, alice: &signer, bob: &signer, carol: &signer, marlon_rando: &signer) { // Scenario: The multisig gets initiated with the 2 validators as the only authorities. IT takes 2-of-2 to sign. // later they add a third (Rando) so it becomes a 2-of-3. // Rando and Bob, then remove alice so it becomes 2-of-2 again @@ -253,7 +431,7 @@ module ol_framework::test_multi_action { let vals = mock::genesis_n_vals(root, 2); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); // Dave creates the resource account. HE is not one of the validators, and is not an authority in the multisig. - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 7357001); @@ -298,15 +476,15 @@ module ol_framework::test_multi_action { assert!(!multi_action::is_authority(new_resource_address, signer::address_of(alice)), 7357004); } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d)] - fun governance_change_threshold(root: &signer, alice: &signer, bob: &signer, dave: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun governance_change_threshold(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // Scenario: The multisig gets initiated with the 2 validators as the only authorities. IT takes 2-of-2 to sign. // They decide next only 1-of-2 will be needed. let vals = mock::genesis_n_vals(root, 2); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); // Dave creates the resource account. HE is not one of the validators, and is not an authority in the multisig. - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 7357001); diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 2988cf0e3..cb6d8eb82 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -70,10 +70,17 @@ module ol_framework::multi_action { /// Duplicate vote const EDUPLICATE_VOTE: u64 = 14; + /// Offer expired + const EOFFER_EXPIRED: u64 = 15; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; + /// default setting for an offer to expire + const DEFAULT_EPOCHS_OFFER_EXPIRE: u64 = 7; + + const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 2; + /// A Governance account is an account which requires multiple votes from Authorities to send a transaction. /// A multisig can be used to get agreement on different types of Actions, such as a payment transaction where the handler code for the transaction is an a separate contract. See for example MultiSigPayment. /// Governance struct holds the metadata for all the instances of Actions on this account. @@ -121,6 +128,77 @@ module ol_framework::multi_action { expiration_epoch: u64, } + /// Offer struct to manage the proposal and claiming of new authorities. + /// - proposed: List of addresses proposed for new authority roles. + /// - claimed: List of addresses that have claimed their proposed roles. + /// - expiration_epoch: The epoch when the offer expires. + struct Offer has key, store, drop { + proposed: vector
, + claimed: vector
, + expiration_epoch: u64, + } + + // Proposes a new offer for authorities while the account is not yet initialized as multi_action. + // - sig: The signer proposing the offer. + // - proposed: The list of addresses proposed for new authority roles. + // - duration_epochs: The duration in epochs before the offer expires. + public fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) { + let addr = signer::address_of(sig); + + // Ensure the account has governance initialized + assert!(is_gov_init(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + + // Ensure the account is not yet initialized as multisig + assert!(!multisig_account::is_multisig(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + + let duration_epochs = if (option::is_some(&duration_epochs)) { + *option::borrow(&duration_epochs) + } else { + DEFAULT_EPOCHS_OFFER_EXPIRE + }; + + let expiration_epoch = epoch_helper::get_current_epoch() + duration_epochs; + let offer = Offer { + proposed, + claimed: vector::empty
(), + expiration_epoch, + }; + + move_to(sig, offer); + } + + // Allows a proposed authority to claim their role. + // - sig: The signer making the claim. + // - multisig_address: The address of the multisig account. + public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer { + let sender_addr = signer::address_of(sig); + let offer = borrow_global_mut(multisig_address); + + // Ensure the sender is in the proposed list + assert!(vector::contains(&offer.proposed, &sender_addr), error::invalid_argument(ENOT_AUTHORIZED)); + + // Ensure the offer has not expired + let current_epoch = epoch_helper::get_current_epoch(); + assert!(offer.expiration_epoch > current_epoch, EOFFER_EXPIRED); + + // Remove the sender from the proposed list and add to the claimed list + let (_, i) = vector::index_of(&offer.proposed, &sender_addr); + vector::remove(&mut offer.proposed, i); + vector::push_back(&mut offer.claimed, sender_addr); + } + + /// Cleans up expired offers. + /// - `multisig_address`: The address of the multisig account. + /* + public fun cleanup_expired_offers(multisig_address: address) acquires Offer { + let current_epoch = epoch_helper::get_current_epoch(); + let offer = borrow_global_mut(multisig_address); + if (offer.expiration_epoch <= current_epoch) { + move_from(multisig_address); + } + } + */ public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { @@ -175,17 +253,42 @@ module ol_framework::multi_action { }; } - /// finalize the account and put in a cage. Will abort if governance has not - // been initialized + /// Finalize the multisign account and put in a cage. Will abort + // if governance has not been initialized and + // if there are not enough claimed authorities. + public fun finalize_and_cage2(sig: &signer) acquires Offer { + let addr = signer::address_of(sig); + + // check governance + assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + + // check claimed authorities + assert!(exists(addr), error::invalid_argument(ENO_SIGNERS)); + assert!(has_enough_offer_claimed(addr), error::invalid_argument(ENO_SIGNERS)); + + // check it is not yet initialized + assert!(!multisig_account::is_multisig(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + + // finalize the account + let initial_authorities = get_offer_claimed(addr); + multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); + + // remove offer + move_from(addr); + } + + // TODO: Remove this public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { let addr = signer::address_of(sig); - assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(exists(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); assert!(exists>(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); + error::invalid_argument(EGOV_NOT_INITIALIZED)); // not yet initialized assert!(!multisig_account::is_multisig(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); + error::invalid_argument(EGOV_NOT_INITIALIZED)); multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); } @@ -206,12 +309,37 @@ module ol_framework::multi_action { assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); } - fun is_gov_init(addr: address): bool { exists(addr) && exists>(addr) } + // Query if an offer exists for the given multisig address. + public fun exists_offer(multisig_address: address): bool { + exists(multisig_address) + } + + // Query proposed authorities for the given multisig address. + public fun get_offer_proposed(multisig_address: address): vector
acquires Offer { + borrow_global(multisig_address).proposed + } + + // Query claimed authorities for the given multisig address. + public fun get_offer_claimed(multisig_address: address): vector
acquires Offer { + borrow_global(multisig_address).claimed + } + + // Query offer expiration epoch. + public fun get_offer_expiration_epoch(multisig_address: address): u64 acquires Offer { + borrow_global(multisig_address).expiration_epoch + } + + // Query if the offer has enough claimed authorities to cage the account. + fun has_enough_offer_claimed(multisig_address: address): bool acquires Offer { + let claimed = get_offer_claimed(multisig_address); + vector::length(&claimed) >= MIN_OFFER_CLAIMS_TO_CAGE + } + /// Has a multisig struct for a given action been created? public(friend) fun has_action(addr: address):bool { exists>(addr) From 7846aad33a8e17dd5e00a6ab1c6121bbfd25bca9 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:47:08 -0300 Subject: [PATCH 04/68] set error codes for offer functions --- .../ol_sources/vote_lib/multi_action.move | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index cb6d8eb82..8038c6d54 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -69,9 +69,16 @@ module ol_framework::multi_action { const EEMPTY_ADDRESSES: u64 = 13; /// Duplicate vote const EDUPLICATE_VOTE: u64 = 14; - /// Offer expired const EOFFER_EXPIRED: u64 = 15; + /// Not offered to initial authorities + const ENOT_OFFERED: u64 = 16; + /// Not enough claimed authorities + const ENOT_ENOUGH_CLAIMED: u64 = 17; + /// Account is already a multisig + const EALREADY_MULTISIG: u64 = 18; + /// Address not proposed for authority role + const EADDRESS_NOT_PROPOSED: u64 = 19; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; @@ -149,8 +156,7 @@ module ol_framework::multi_action { assert!(is_gov_init(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); // Ensure the account is not yet initialized as multisig - assert!(!multisig_account::is_multisig(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(!multisig_account::is_multisig(addr), error::invalid_argument(EALREADY_MULTISIG)); let duration_epochs = if (option::is_some(&duration_epochs)) { *option::borrow(&duration_epochs) @@ -176,7 +182,7 @@ module ol_framework::multi_action { let offer = borrow_global_mut(multisig_address); // Ensure the sender is in the proposed list - assert!(vector::contains(&offer.proposed, &sender_addr), error::invalid_argument(ENOT_AUTHORIZED)); + assert!(vector::contains(&offer.proposed, &sender_addr), error::invalid_argument(EADDRESS_NOT_PROPOSED)); // Ensure the offer has not expired let current_epoch = epoch_helper::get_current_epoch(); @@ -188,18 +194,6 @@ module ol_framework::multi_action { vector::push_back(&mut offer.claimed, sender_addr); } - /// Cleans up expired offers. - /// - `multisig_address`: The address of the multisig account. - /* - public fun cleanup_expired_offers(multisig_address: address) acquires Offer { - let current_epoch = epoch_helper::get_current_epoch(); - let offer = borrow_global_mut(multisig_address); - if (offer.expiration_epoch <= current_epoch) { - move_from(multisig_address); - } - } - */ - public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { let duration_epochs = if (option::is_some(&duration_epochs)) { @@ -263,13 +257,13 @@ module ol_framework::multi_action { assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); - // check claimed authorities - assert!(exists(addr), error::invalid_argument(ENO_SIGNERS)); - assert!(has_enough_offer_claimed(addr), error::invalid_argument(ENO_SIGNERS)); - // check it is not yet initialized assert!(!multisig_account::is_multisig(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + // check claimed authorities + assert!(exists(addr), error::invalid_argument(ENOT_OFFERED)); + assert!(has_enough_offer_claimed(addr), error::invalid_argument(ENOT_ENOUGH_CLAIMED)); + // finalize the account let initial_authorities = get_offer_claimed(addr); multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); From 3c6b191ce8516e028759b004c9242ad25e807283 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Thu, 13 Jun 2024 08:45:13 -0300 Subject: [PATCH 05/68] adds more tests to multi action offer --- .../tests/vote_lib/multi_action.test.move | 164 ++++++++++++------ .../ol_sources/vote_lib/multi_action.move | 52 +++++- 2 files changed, 151 insertions(+), 65 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index d44ca25e1..de3c33422 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -131,86 +131,138 @@ module ol_framework::test_multi_action { assert!(!multi_action::exists_offer(carol_address), 0); } + // Try to propose offer without governance + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 1, location = ol_framework::multi_action)] + fun propose_offer_without_gov(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + } + + // Try to propose offer to an multisig account + #[test(root = @ol_framework, dave = @0x1000d, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 18, location = ol_framework::multi_action)] + fun propose_offer_to_multisign(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let carol_address = @0x1000c; + let dave_address = @0x1000d; + multi_action::init_gov(carol); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + multi_action::finalize_and_cage2(carol); + + // propose offer to multisig account + multi_action::propose_offer(carol, vector::singleton(dave_address), option::none()); + } + + // Try to propose an empty offer + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 16, location = ol_framework::multi_action)] + fun propose_empty_offer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + multi_action::init_gov(alice); + multi_action::propose_offer(alice, vector::empty
(), option::none()); + } + + // Try to propose offer to the signer address + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 2, location = ol_framework::multi_action)] + fun propose_offer_to_signer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let alice_address = signer::address_of(alice); + multi_action::init_gov(alice); + multi_action::propose_offer(alice, vector::singleton
(alice_address), option::none()); + } + + // Try to propose offer to an invalid signer + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 20, location = ol_framework::multi_action)] + fun offer_to_invalid_authority(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + multi_action::init_gov(alice); + + // propose to invalid address + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, @0xCAFE); + multi_action::propose_offer(alice, authorities, option::some(2)); + } + + // Try to propose offer with zero duration epochs + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 22, location = ol_framework::multi_action)] + fun offer_with_zero_duration(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + multi_action::init_gov(alice); + + // propose to invalid address + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(alice, authorities, option::some(0)); + } + // Try to claim expired offer #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 15, location = ol_framework::multi_action)] fun claim_expired_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - - // initialize the multi_action account - multi_action::init_gov(&resource_sig); - + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + multi_action::init_gov(carol); + // invite the vals to the resource account let authorities = vector::empty
(); vector::push_back(&mut authorities, signer::address_of(alice)); vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(&resource_sig, authorities, option::some(2)); + multi_action::propose_offer(carol, authorities, option::some(2)); // alice claim the offer - multi_action::claim_offer(alice, new_resource_address); + multi_action::claim_offer(alice, carol_address); mock::trigger_epoch(root); // epoch 1 valid mock::trigger_epoch(root); // epoch 2 valid mock::trigger_epoch(root); // epoch 3 expired // bob claim expired offer - multi_action::claim_offer(bob, new_resource_address); + multi_action::claim_offer(bob, carol_address); } - #[test] - fun test_adal() { - let authorities: vector
= vector::empty(); - let alice = @0x1000a; - let bob = @0x1000b; - vector::push_back(&mut authorities, alice); - vector::push_back(&mut authorities, bob); - assert!(vector::length(&authorities) == 2, 0); - assert!(*vector::borrow(&authorities, 0) == alice, 0); - print(&authorities); - - let x = 0; - let y = 1; - let r = if (false) { - &mut x - } else { - &mut y - }; - *r = *r + 1; - assert!(*r == 2, 0); - print(r); - - let text = x"ADA1"; - print(&text); - - let a = 0; - let b = copy a + 1; - let c = a + 2; - - print(&b); - print(&c); + // Try to claim offer of an account without proposal + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 17, location = ol_framework::multi_action)] + fun claim_offer_without_proposal(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let bob_address = @0x1000c; + multi_action::claim_offer(alice, bob_address); } - /* - #[test(carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - fun offer_authorities(carol: &signer, alice: &signer, bob: &signer) { - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 0); - - print(&1111111); - - // offer authorities to resource_sig for alice and bob + // Try to claim offer twice + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 23, location = ol_framework::multi_action)] + fun claim_offer_twice(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + multi_action::init_gov(carol); + + // invite the vals to the resource account let authorities = vector::empty
(); vector::push_back(&mut authorities, signer::address_of(alice)); vector::push_back(&mut authorities, signer::address_of(bob)); - // multi_action::propose_offer(&resource_sig, authorities, 7); - - //print(&resource_sig); - - }*/ + multi_action::propose_offer(carol, authorities, option::some(2)); + // alice claim the offer + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(alice, carol_address); + } + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a)] fun propose_action(root: &signer, carol: &signer, alice: &signer) { diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 8038c6d54..ece903e7c 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -71,21 +71,28 @@ module ol_framework::multi_action { const EDUPLICATE_VOTE: u64 = 14; /// Offer expired const EOFFER_EXPIRED: u64 = 15; + /// Offer empty + const EOFFER_EMPTY: u64 = 16; /// Not offered to initial authorities - const ENOT_OFFERED: u64 = 16; + const ENOT_OFFERED: u64 = 17; /// Not enough claimed authorities - const ENOT_ENOUGH_CLAIMED: u64 = 17; + const ENOT_ENOUGH_CLAIMED: u64 = 18; /// Account is already a multisig - const EALREADY_MULTISIG: u64 = 18; + const EALREADY_MULTISIG: u64 = 19; /// Address not proposed for authority role - const EADDRESS_NOT_PROPOSED: u64 = 19; + const EADDRESS_NOT_PROPOSED: u64 = 20; + /// Address proposed for authority role does not exist + const EPROPOSED_NOT_EXISTS: u64 = 21; + /// Offer duration must be greater than zero + const EZERO_DURATION: u64 = 22; + /// Offer already claimed + const EALREADY_CLAIMED: u64 = 23; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; - /// default setting for an offer to expire const DEFAULT_EPOCHS_OFFER_EXPIRE: u64 = 7; - + /// minimum number of claimed authorities to cage the account const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 2; /// A Governance account is an account which requires multiple votes from Authorities to send a transaction. @@ -153,10 +160,27 @@ module ol_framework::multi_action { let addr = signer::address_of(sig); // Ensure the account has governance initialized - assert!(is_gov_init(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(is_gov_init(addr), EGOV_NOT_INITIALIZED); // Ensure the account is not yet initialized as multisig - assert!(!multisig_account::is_multisig(addr), error::invalid_argument(EALREADY_MULTISIG)); + assert!(!multisig_account::is_multisig(addr), EALREADY_MULTISIG); + + // Ensure the proposed list is not empty + assert!(vector::length(&proposed) > 0, EOFFER_EMPTY); + + // Ensure the proposed list does not contain the signer + assert!(!vector::contains(&proposed, &addr), ESIGNER_CANT_BE_AUTHORITY); + + // Ensure the proposed list address are valid + let i = 0; + while (i < vector::length(&proposed)) { + let proposed_addr = vector::borrow(&proposed, i); + assert!(account::exists_at(*proposed_addr), EPROPOSED_NOT_EXISTS); + i = i + 1; + }; + + // Ensure the offer has not yet been proposed + // assert!(!exists(addr), error::invalid_argument(EDUPLICATE_PROPOSAL)); let duration_epochs = if (option::is_some(&duration_epochs)) { *option::borrow(&duration_epochs) @@ -164,6 +188,9 @@ module ol_framework::multi_action { DEFAULT_EPOCHS_OFFER_EXPIRE }; + // Ensure duration is greater than zero + assert!(duration_epochs > 0, EZERO_DURATION); + let expiration_epoch = epoch_helper::get_current_epoch() + duration_epochs; let offer = Offer { proposed, @@ -179,10 +206,17 @@ module ol_framework::multi_action { // - multisig_address: The address of the multisig account. public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer { let sender_addr = signer::address_of(sig); + + // Ensure the account has an offer + assert!(exists(multisig_address), ENOT_OFFERED); + let offer = borrow_global_mut(multisig_address); + // Ensure the sender is not in the claimed list + assert!(!vector::contains(&offer.claimed, &sender_addr), EALREADY_CLAIMED); + // Ensure the sender is in the proposed list - assert!(vector::contains(&offer.proposed, &sender_addr), error::invalid_argument(EADDRESS_NOT_PROPOSED)); + assert!(vector::contains(&offer.proposed, &sender_addr), EADDRESS_NOT_PROPOSED); // Ensure the offer has not expired let current_epoch = epoch_helper::get_current_epoch(); From e9892ff18ee8d60793ce0087423e1150ea6a2679 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Thu, 13 Jun 2024 10:34:27 -0300 Subject: [PATCH 06/68] applies canonical errors to offer functions, and creates more tests --- .../tests/vote_lib/multi_action.test.move | 96 +++++++++++++++-- .../ol_sources/vote_lib/multi_action.move | 101 +++++++++--------- 2 files changed, 137 insertions(+), 60 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index de3c33422..6565213c9 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -133,7 +133,7 @@ module ol_framework::test_multi_action { // Try to propose offer without governance #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 1, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] fun propose_offer_without_gov(root: &signer, carol: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); @@ -146,7 +146,7 @@ module ol_framework::test_multi_action { // Try to propose offer to an multisig account #[test(root = @ol_framework, dave = @0x1000d, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 18, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x30019, location = ol_framework::multi_action)] fun propose_offer_to_multisign(root: &signer, carol: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 4); let carol_address = @0x1000c; @@ -166,7 +166,7 @@ module ol_framework::test_multi_action { // Try to propose an empty offer #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 16, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x10016, location = ol_framework::multi_action)] fun propose_empty_offer(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 4); multi_action::init_gov(alice); @@ -175,7 +175,7 @@ module ol_framework::test_multi_action { // Try to propose offer to the signer address #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 2, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x50002, location = ol_framework::multi_action)] fun propose_offer_to_signer(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 4); let alice_address = signer::address_of(alice); @@ -185,7 +185,7 @@ module ol_framework::test_multi_action { // Try to propose offer to an invalid signer #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 20, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x60021, location = ol_framework::multi_action)] fun offer_to_invalid_authority(root: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); @@ -199,7 +199,7 @@ module ol_framework::test_multi_action { // Try to propose offer with zero duration epochs #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 22, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x10022, location = ol_framework::multi_action)] fun offer_with_zero_duration(root: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); @@ -210,9 +210,24 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(0)); } + // Try to claim offer not offered to signer + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x60020, location = ol_framework::multi_action)] + fun claim_offer_not_offered(root: &signer, alice: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + multi_action::init_gov(carol); + + // invite bob + multi_action::propose_offer(carol, vector::singleton(@0x1000b), option::none()); + + // alice try to claim the offer + multi_action::claim_offer(alice, carol_address); + } + // Try to claim expired offer #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 15, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x20015, location = ol_framework::multi_action)] fun claim_expired_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; @@ -237,7 +252,7 @@ module ol_framework::test_multi_action { // Try to claim offer of an account without proposal #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 17, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x60017, location = ol_framework::multi_action)] fun claim_offer_without_proposal(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 2); let bob_address = @0x1000c; @@ -246,7 +261,7 @@ module ol_framework::test_multi_action { // Try to claim offer twice #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 23, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x80023, location = ol_framework::multi_action)] fun claim_offer_twice(root: &signer, carol: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; @@ -258,10 +273,71 @@ module ol_framework::test_multi_action { vector::push_back(&mut authorities, signer::address_of(bob)); multi_action::propose_offer(carol, authorities, option::some(2)); - // alice claim the offer + // Alice claim the offer twice multi_action::claim_offer(alice, carol_address); multi_action::claim_offer(alice, carol_address); } + + // Try to finalize account without governance + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] + fun finalize_without_gov(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + multi_action::finalize_and_cage2(alice); + } + + // Try to finalize account without offer + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x60017, location = ol_framework::multi_action)] + fun finalize_without_offer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + multi_action::init_gov(alice); + multi_action::finalize_and_cage2(alice); + } + + // Try to finalize account without enough offer claimed + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x30018, location = ol_framework::multi_action)] + fun finalize_without_enough_claimed(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + multi_action::init_gov(alice); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + + // bob claim the offer + multi_action::claim_offer(bob, alice_address); + + // finalize the multi_action account + multi_action::finalize_and_cage2(alice); + } + + // Try to finalize account already finalized + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x80019, location = ol_framework::multi_action)] + fun finalize_already_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + multi_action::init_gov(alice); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + + // bob claim the offer + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + + // finalize the multi_action account + multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage2(alice); + } #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a)] fun propose_action(root: &signer, carol: &signer, alice: &signer) { diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index ece903e7c..9127e3c0c 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -40,11 +40,11 @@ module ol_framework::multi_action { #[test_only] friend ol_framework::test_multi_action; -// use diem_std::debug::print; + // use diem_std::debug::print; - const EGOV_NOT_INITIALIZED: u64 = 1; + const EGOV_NOT_INITIALIZED: u64 = 0x1; /// The owner of this account can't be an authority, since it will subsequently be bricked. The signer of this account is no longer useful. The account is now controlled by the Governance logic. - const ESIGNER_CANT_BE_AUTHORITY: u64 = 2; + const ESIGNER_CANT_BE_AUTHORITY: u64 = 0x2; /// signer not authorized to approve a transaction. const ENOT_AUTHORIZED: u64 = 3; /// There are no pending transactions to search @@ -70,23 +70,23 @@ module ol_framework::multi_action { /// Duplicate vote const EDUPLICATE_VOTE: u64 = 14; /// Offer expired - const EOFFER_EXPIRED: u64 = 15; + const EOFFER_EXPIRED: u64 = 0x15; /// Offer empty - const EOFFER_EMPTY: u64 = 16; + const EOFFER_EMPTY: u64 = 0x16; /// Not offered to initial authorities - const ENOT_OFFERED: u64 = 17; + const ENOT_OFFERED: u64 = 0x17; /// Not enough claimed authorities - const ENOT_ENOUGH_CLAIMED: u64 = 18; + const ENOT_ENOUGH_CLAIMED: u64 = 0x18; /// Account is already a multisig - const EALREADY_MULTISIG: u64 = 19; + const EALREADY_MULTISIG: u64 = 0x19; /// Address not proposed for authority role - const EADDRESS_NOT_PROPOSED: u64 = 20; + const EADDRESS_NOT_PROPOSED: u64 = 0x20; /// Address proposed for authority role does not exist - const EPROPOSED_NOT_EXISTS: u64 = 21; + const EPROPOSED_NOT_EXISTS: u64 = 0x21; /// Offer duration must be greater than zero - const EZERO_DURATION: u64 = 22; + const EZERO_DURATION: u64 = 0x22; /// Offer already claimed - const EALREADY_CLAIMED: u64 = 23; + const EALREADY_CLAIMED: u64 = 0x23; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; @@ -159,23 +159,23 @@ module ol_framework::multi_action { public fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) { let addr = signer::address_of(sig); - // Ensure the account has governance initialized - assert!(is_gov_init(addr), EGOV_NOT_INITIALIZED); - // Ensure the account is not yet initialized as multisig - assert!(!multisig_account::is_multisig(addr), EALREADY_MULTISIG); + assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); + + // Ensure the account has governance initialized + assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); // Ensure the proposed list is not empty - assert!(vector::length(&proposed) > 0, EOFFER_EMPTY); + assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); // Ensure the proposed list does not contain the signer - assert!(!vector::contains(&proposed, &addr), ESIGNER_CANT_BE_AUTHORITY); + assert!(!vector::contains(&proposed, &addr), error::permission_denied(ESIGNER_CANT_BE_AUTHORITY)); // Ensure the proposed list address are valid let i = 0; while (i < vector::length(&proposed)) { let proposed_addr = vector::borrow(&proposed, i); - assert!(account::exists_at(*proposed_addr), EPROPOSED_NOT_EXISTS); + assert!(account::exists_at(*proposed_addr), error::not_found(EPROPOSED_NOT_EXISTS)); i = i + 1; }; @@ -189,7 +189,7 @@ module ol_framework::multi_action { }; // Ensure duration is greater than zero - assert!(duration_epochs > 0, EZERO_DURATION); + assert!(duration_epochs > 0, error::invalid_argument(EZERO_DURATION)); let expiration_epoch = epoch_helper::get_current_epoch() + duration_epochs; let offer = Offer { @@ -204,23 +204,23 @@ module ol_framework::multi_action { // Allows a proposed authority to claim their role. // - sig: The signer making the claim. // - multisig_address: The address of the multisig account. - public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer { + public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer { let sender_addr = signer::address_of(sig); // Ensure the account has an offer - assert!(exists(multisig_address), ENOT_OFFERED); + assert!(exists(multisig_address), error::not_found(ENOT_OFFERED)); let offer = borrow_global_mut(multisig_address); // Ensure the sender is not in the claimed list - assert!(!vector::contains(&offer.claimed, &sender_addr), EALREADY_CLAIMED); + assert!(!vector::contains(&offer.claimed, &sender_addr), error::already_exists(EALREADY_CLAIMED)); // Ensure the sender is in the proposed list - assert!(vector::contains(&offer.proposed, &sender_addr), EADDRESS_NOT_PROPOSED); + assert!(vector::contains(&offer.proposed, &sender_addr), error::not_found(EADDRESS_NOT_PROPOSED)); // Ensure the offer has not expired let current_epoch = epoch_helper::get_current_epoch(); - assert!(offer.expiration_epoch > current_epoch, EOFFER_EXPIRED); + assert!(offer.expiration_epoch > current_epoch, error::out_of_range(EOFFER_EXPIRED)); // Remove the sender from the proposed list and add to the claimed list let (_, i) = vector::index_of(&offer.proposed, &sender_addr); @@ -228,6 +228,32 @@ module ol_framework::multi_action { vector::push_back(&mut offer.claimed, sender_addr); } + /// Finalizes the multisign account and locks it (cage). + /// - sig: The signer finalizing the account. + /// Aborts if governance is not initialized, the account is already a multisig, + /// there are not enough claimed authorities, or the offer is not found. + public fun finalize_and_cage2(sig: &signer) acquires Offer { + let addr = signer::address_of(sig); + + // check it is not yet initialized + assert!(!multisig_account::is_multisig(addr), error::already_exists(EALREADY_MULTISIG)); + + // check governance + assert!(exists(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + + // check claimed authorities + assert!(exists(addr), error::not_found(ENOT_OFFERED)); + assert!(has_enough_offer_claimed(addr), error::invalid_state(ENOT_ENOUGH_CLAIMED)); + + // finalize the account + let initial_authorities = get_offer_claimed(addr); + multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); + + // remove offer + move_from(addr); + } + public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { let duration_epochs = if (option::is_some(&duration_epochs)) { @@ -281,31 +307,6 @@ module ol_framework::multi_action { }; } - /// Finalize the multisign account and put in a cage. Will abort - // if governance has not been initialized and - // if there are not enough claimed authorities. - public fun finalize_and_cage2(sig: &signer) acquires Offer { - let addr = signer::address_of(sig); - - // check governance - assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); - assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); - - // check it is not yet initialized - assert!(!multisig_account::is_multisig(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); - - // check claimed authorities - assert!(exists(addr), error::invalid_argument(ENOT_OFFERED)); - assert!(has_enough_offer_claimed(addr), error::invalid_argument(ENOT_ENOUGH_CLAIMED)); - - // finalize the account - let initial_authorities = get_offer_claimed(addr); - multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); - - // remove offer - move_from(addr); - } - // TODO: Remove this public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { From dffd5daf3a9cb6645526bc4ef586f4bfa6c2f80e Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:45:39 -0300 Subject: [PATCH 07/68] supports submit new offers, and adds more tests --- .../tests/vote_lib/multi_action.test.move | 121 ++++++++++++++++++ .../ol_sources/vote_lib/multi_action.move | 52 ++++++-- 2 files changed, 161 insertions(+), 12 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 6565213c9..d3707bece 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -51,6 +51,7 @@ module ol_framework::test_multi_action { // check the offer is proposed and account is not muti_action yet assert!(multi_action::exists_offer(carol_address), 0); assert!(multi_action::get_offer_proposed(carol_address) == authorities, 0); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 0); assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 0); assert!(multi_action::get_offer_expiration_epoch(carol_address) == 3, 0); assert!(!multi_action::is_multi_action(carol_address), 0); @@ -131,6 +132,126 @@ module ol_framework::test_multi_action { assert!(!multi_action::exists_offer(carol_address), 0); } + // Propose another offer with different authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun propose_another_offer_different_authorities(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + multi_action::init_gov(alice); + + // invite bob + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(1)); + + // invite carol and dave + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + multi_action::propose_offer(alice, authorities, option::some(2)); + + // check new authorities + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 2, 0); + print(&multi_action::get_offer_proposed(@0x1000a)); + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 0); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 0); + } + + // Propose new offer with more authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun propose_offer_more_authorities(root: &signer, alice: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + multi_action::init_gov(alice); + + // invite bob + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(1)); + + // new invite bob and carol + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x1000b); + vector::push_back(&mut authorities, @0x1000c); + multi_action::propose_offer(alice, authorities, option::some(2)); + + // check new authorities + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 0); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 0); + + // carol claim the offer + multi_action::claim_offer(carol, @0x1000a); + + // new invite bob, carol and dave + vector::push_back(&mut authorities, @0x1000d); + multi_action::propose_offer(alice, authorities, option::some(3)); + + // check new authorities + let proposed = vector::empty(); + vector::push_back(&mut proposed, @0x1000b); + vector::push_back(&mut proposed, @0x1000d); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 3, 0); + assert!(multi_action::get_offer_proposed(@0x1000a) == proposed, 0); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 0); + } + + // Propose new offer with less authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun propose_offer_less_authorities(root: &signer, alice: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + multi_action::init_gov(alice); + + // invite bob, carol and dave + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + multi_action::propose_offer(alice, authorities, option::some(2)); + + // new invite bob and carol + let new_authorities = vector::empty
(); + vector::push_back(&mut new_authorities, @0x1000b); + vector::push_back(&mut new_authorities, @0x1000c); + multi_action::propose_offer(alice, new_authorities, option::some(3)); + + // check new authorities minus dave + assert!(multi_action::get_offer_proposed(@0x1000a) == new_authorities, 0); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 0); + + // carol claim the offer + multi_action::claim_offer(carol, @0x1000a); + + // new invite carol + multi_action::propose_offer(alice, vector::singleton(@0x1000c), option::some(4)); + + // check new authorities minus bob + assert!(multi_action::get_offer_proposed(@0x1000a) == vector::empty(), 0); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 0); + } + + // Propose new offer with same authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun propose_offer_same_authorities(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + multi_action::init_gov(alice); + + // invite bob and carol + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x1000b); + vector::push_back(&mut authorities, @0x1000c); + multi_action::propose_offer(alice, authorities, option::some(2)); + + // new invite bob and carol + multi_action::propose_offer(alice, authorities, option::some(3)); + + // check authorities + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 0); + + // bob claim the offer + multi_action::claim_offer(bob, @0x1000a); + + // new invite bob and carol + multi_action::propose_offer(alice, authorities, option::some(4)); + + // check authorities + assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000c), 0); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000b), 0); + } + // Try to propose offer without governance #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 9127e3c0c..30971b549 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -156,7 +156,7 @@ module ol_framework::multi_action { // - sig: The signer proposing the offer. // - proposed: The list of addresses proposed for new authority roles. // - duration_epochs: The duration in epochs before the offer expires. - public fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) { + public fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer{ let addr = signer::address_of(sig); // Ensure the account is not yet initialized as multisig @@ -178,10 +178,7 @@ module ol_framework::multi_action { assert!(account::exists_at(*proposed_addr), error::not_found(EPROPOSED_NOT_EXISTS)); i = i + 1; }; - - // Ensure the offer has not yet been proposed - // assert!(!exists(addr), error::invalid_argument(EDUPLICATE_PROPOSAL)); - + let duration_epochs = if (option::is_some(&duration_epochs)) { *option::borrow(&duration_epochs) } else { @@ -192,13 +189,44 @@ module ol_framework::multi_action { assert!(duration_epochs > 0, error::invalid_argument(EZERO_DURATION)); let expiration_epoch = epoch_helper::get_current_epoch() + duration_epochs; - let offer = Offer { - proposed, - claimed: vector::empty
(), - expiration_epoch, - }; - - move_to(sig, offer); + + // Update the offer if exists, otherwise create a new one + if (exists(addr)) { + // Update offer + let offer = borrow_global_mut(addr); + + // Remove claimed addresses that are not in the new proposed list + let j = 0; + while (j < vector::length(&offer.claimed)) { + let claimed_addr = vector::borrow(&offer.claimed, j); + if (!vector::contains(&proposed, claimed_addr)) { + vector::remove(&mut offer.claimed, j); + } else { + j = j + 1; + }; + }; + + // Remove new proposed addresses that are already claimed + let i = 0; + while (i < vector::length(&proposed)) { + let proposed_addr = vector::borrow(&proposed, i); + if (vector::contains(&offer.claimed, proposed_addr)) { + vector::remove(&mut proposed, i); + }; + i = i + 1; + }; + + offer.proposed = proposed; + offer.expiration_epoch = expiration_epoch; + } else { + // create new offer + let offer = Offer { + proposed, + claimed: vector::empty
(), + expiration_epoch, + }; + move_to(sig, offer); + } } // Allows a proposed authority to claim their role. From 852e7b46eb87efc2951b6e3de2cab31e153ad5f3 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:51:13 -0300 Subject: [PATCH 08/68] applies new offer flow to the other tests --- .../tests/vote_lib/multi_action.test.move | 299 +++++++++--------- .../ol_sources/vote_lib/multi_action.move | 71 ++--- 2 files changed, 189 insertions(+), 181 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index d3707bece..e90187389 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -248,6 +248,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(4)); // check authorities + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 4, 0); assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000c), 0); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000b), 0); } @@ -460,201 +461,205 @@ module ol_framework::test_multi_action { multi_action::finalize_and_cage2(alice); } - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a)] - fun propose_action(root: &signer, carol: &signer, alice: &signer) { - - let vals = mock::genesis_n_vals(root, 2); - // mock::ol_initialize_coin(root); - - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 0); - - // make the vals the signers on the safe - // SO ALICE IS AUTHORIZED - multi_action::init_gov(&resource_sig); - multi_action::init_type(&resource_sig, true); + // Happy Day: propose a new action and check zero votes + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun propose_action(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; - //need to be caged to finalize multi action workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); + // offer to bob and carol authority on the alice safe + multi_action::init_gov(alice); + multi_action::init_type(alice, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + + // bob and alice claim the offer + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + + // alice finalize multi action workflow to release control of the account + multi_action::finalize_and_cage2(alice); - // create a proposal + // bob create a proposal let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - let id = multi_action::propose_new(alice, new_resource_address, proposal); + let id = multi_action::propose_new(bob, alice_address, proposal); // SHOULD NOT HAVE COUNTED ANY VOTES - let v = multi_action::get_votes(new_resource_address, guid::id_creation_num(&id)); + let v = multi_action::get_votes(alice_address, guid::id_creation_num(&id)); assert!(vector::length(&v) == 0, 7357003); } - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000a)] + // Multisign authorities bob and carol try to send the same proposal + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] fun propose_action_prevent_duplicated(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - // Scenario: alice and bob are authorities. They try to send the same proposal - let vals = mock::genesis_n_vals(root, 2); - // mock::ol_initialize_coin(root); - - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 0); + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; - // make the vals the signers on the safe - // SO ALICE IS AUTHORIZED - multi_action::init_gov(&resource_sig); - multi_action::init_type(&resource_sig, true); + // offer to bob and carol authority on the alice safe + multi_action::init_gov(alice); + multi_action::init_type(alice, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); - //need to be caged to finalize multi action workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); + // bob and alice claim the offer + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + + // alice finalize multi action workflow to release control of the account + multi_action::finalize_and_cage2(alice); - let count = multi_action::get_count_of_pending(new_resource_address); + let count = multi_action::get_count_of_pending(alice_address); assert!(count == 0, 7357001); let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - let id = multi_action::propose_new(alice, new_resource_address, proposal); - let count = multi_action::get_count_of_pending(new_resource_address); + let id = multi_action::propose_new(bob, alice_address, proposal); + let count = multi_action::get_count_of_pending(alice_address); assert!(count == 1, 7357002); - let epoch_ending = multi_action::get_expiration(new_resource_address, guid::id_creation_num(&id)); + let epoch_ending = multi_action::get_expiration(alice_address, guid::id_creation_num(&id)); assert!(epoch_ending == 14, 7357003); let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - multi_action::propose_new(bob, new_resource_address, proposal); - let count = multi_action::get_count_of_pending(new_resource_address); + multi_action::propose_new(carol, alice_address, proposal); + let count = multi_action::get_count_of_pending(alice_address); assert!(count == 1, 7357005); // no change // confirm there are no votes - let v = multi_action::get_votes(new_resource_address, guid::id_creation_num(&id)); + let v = multi_action::get_votes(alice_address, guid::id_creation_num(&id)); assert!(vector::length(&v) == 0, 7357003); // proposing a different ending epoch will have no effect on the proposal once it is started. Here we try to set to epoch 4, but nothing should change, at it will still be 14. let proposal = multi_action::proposal_constructor(DummyType{}, option::some(4)); - multi_action::propose_new(bob, new_resource_address, proposal); - let count = multi_action::get_count_of_pending(new_resource_address); + multi_action::propose_new(carol, alice_address, proposal); + let count = multi_action::get_count_of_pending(alice_address); assert!(count == 1, 7357004); - let epoch = multi_action::get_expiration(new_resource_address, guid::id_creation_num(&id)); + let epoch = multi_action::get_expiration(alice_address, guid::id_creation_num(&id)); assert!(epoch != 4, 7357004); assert!(epoch == 14, 7357005); } + // Happy day: complete vote action #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] fun vote_action_happy_simple(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // Scenario: a simple MultiAction where we don't need any capabilities. Only need to know if the result was successful on the vote that crossed the threshold. - let vals = mock::genesis_n_vals(root, 2); - mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 0); - - // make the vals the signers on the safe, and 2-of-2 need to sign - multi_action::init_gov(&resource_sig); + // transform alice account in multisign with bob and carol as authorities + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + multi_action::init_gov(alice); // Ths is a simple multi_action: there is no capability being stored - multi_action::init_type(&resource_sig, false); - //need to be caged to finalize multi action workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); + multi_action::init_type(alice, false); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); - // create a proposal + // bob create a proposal let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - - let id = multi_action::propose_new(alice, new_resource_address, proposal); - - let (passed, cap_opt) = multi_action::vote_with_id(alice, &id, new_resource_address); + let id = multi_action::propose_new(bob, alice_address, proposal); + let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, alice_address); assert!(passed == false, 7357001); option::destroy_none(cap_opt); - let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, - new_resource_address); - + let (passed, cap_opt) = multi_action::vote_with_id(carol, &id, alice_address); assert!(passed == true, 7357002); - // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED assert!(option::is_none(&cap_opt), 7357003); option::destroy_none(cap_opt); - } - - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + // Happy day: complete vote action with withdraw capability + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] fun vote_action_happy_withdraw_cap(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // Scenario: testing that a payment type multisig could be created with this module: that the WithdrawCapability can be used here. - let vals = mock::genesis_n_vals(root, 2); + let _vals = mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); + let alice_address = @0x1000a; - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, - b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 0); - // fund the multi_action's account - ol_account::transfer(alice, new_resource_address, 100); - - // make the vals the signers on the safe, and 2-of-2 need to sign - multi_action::init_gov(&resource_sig); - multi_action::init_type(&resource_sig, true); - - //need to be caged to finalize multi action workflow and release control of the account + // fund the alice multi_action's account + ol_account::transfer(alice, alice_address, 100); - multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); + // make the bob and carol the signers on the alice safe, and 2-of-2 need to sign + multi_action::init_gov(alice); + multi_action::init_type(alice, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + // bob create a proposal and vote let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - - let id = multi_action::propose_new(alice, new_resource_address, proposal); - - let (passed, cap_opt) = multi_action::vote_with_id(alice, &id, new_resource_address); + let id = multi_action::propose_new(bob, alice_address, proposal); + let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, alice_address); assert!(passed == false, 7357001); option::destroy_none(cap_opt); - let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, new_resource_address); + // carol vote on bob proposal + let (passed, cap_opt) = multi_action::vote_with_id(carol, &id, alice_address); assert!(passed == true, 7357002); // THE WITHDRAW CAPABILITY IS WHERE WE EXPECT assert!(option::is_some(&cap_opt), 7357003); - let cap = option::extract(&mut cap_opt); let c = ol_account::withdraw_with_capability( &cap, 42, ); - ol_account::create_account(root, @0x1000c); - ol_account::deposit_coins(@0x1000c, c); + // deposit to erik account + ol_account::create_account(root, @0x1000e); + ol_account::deposit_coins(@0x1000e, c); option::fill(&mut cap_opt, cap); - - let (_, balance) = ol_account::balance(@0x1000c); + // check erik account balance + let (_, balance) = ol_account::balance(@0x1000e); assert!(balance == 42, 7357004); - multi_action::maybe_restore_withdraw_cap(cap_opt); } - - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 65548, location = ol_framework::multi_action)] - - fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + // Try to vote on a closed ballot + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x30012, location = ol_framework::multi_action)] + fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, dave: &signer) { // Scenario: Testing that if an action expires voting cannot be done. - let vals = mock::genesis_n_vals(root, 2); + let _vals = mock::genesis_n_vals(root, 3); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); + // we are at epoch 0 let epoch = reconfiguration::get_current_epoch(); assert!(epoch == 0, 7357001); - // Dave creates the resource account. He is not one of the validators, and is not an authority in the multisig. - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 7357002); - // fund the account - ol_account::transfer(alice, new_resource_address, 100); - // make the vals the signers on the safe - // SO ALICE and DAVE ARE AUTHORIZED - safe::init_payment_multisig(&resource_sig); // both need to sign + // dave creates erik resource account. He is not one of the validators, and is not an authority in the multisig. + let (erik, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1000e"); + let erik_address = signer::address_of(&erik); + assert!(resource_account::is_resource_account(erik_address), 7357002); - //need to be caged to finalize multi action workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); + // fund the account + ol_account::transfer(alice, erik_address, 100); + // offer alice and bob authority on the safe + safe::init_payment_multisig(&erik); // both need to sign + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(&erik, authorities, option::none()); + multi_action::claim_offer(alice, erik_address); + multi_action::claim_offer(bob, erik_address); + multi_action::finalize_and_cage2(&erik); // make a proposal for governance, expires in 2 epoch from now - let id = multi_action::propose_governance(alice, new_resource_address, vector::empty(), true, option::some(1), option::some(2)); + let id = multi_action::propose_governance(alice, erik_address, vector::empty(), true, option::some(1), option::some(2)); mock::trigger_epoch(root); // epoch 1 mock::trigger_epoch(root); // epoch 2 @@ -666,8 +671,7 @@ module ol_framework::test_multi_action { assert!(epoch == 4, 7357003); // trying to vote on a closed ballot will error - let _passed = multi_action::vote_governance(bob, new_resource_address, &id); - + let _passed = multi_action::vote_governance(bob, erik_address, &id); } @@ -677,7 +681,7 @@ module ol_framework::test_multi_action { // later they add a third (Rando) so it becomes a 2-of-3. // Rando and Bob, then remove alice so it becomes 2-of-2 again - let vals = mock::genesis_n_vals(root, 2); + let _vals = mock::genesis_n_vals(root, 2); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); // Dave creates the resource account. HE is not one of the validators, and is not an authority in the multisig. let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); @@ -686,22 +690,23 @@ module ol_framework::test_multi_action { // fund the account ol_account::transfer(alice, new_resource_address, 100); - // make the vals the signers on the safe - // SO ALICE and BOB ARE AUTHORIZED + // offer alice and bob authority on the safe multi_action::init_gov(&resource_sig);// both need to sign multi_action::init_type(&resource_sig, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(&resource_sig, authorities, option::none()); + multi_action::claim_offer(alice, new_resource_address); + multi_action::claim_offer(bob, new_resource_address); + multi_action::finalize_and_cage2(&resource_sig); - //need to be caged to finalize multi action workflow and release control of - // the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); - - // Alice is going to propose to change the authorities to add Rando + // alice is going to propose to change the authorities to add Rando let id = multi_action::propose_governance(alice, new_resource_address, - vector::singleton(signer::address_of(marlon_rando)), true, option::none(), - option::none()); + vector::singleton(signer::address_of(marlon_rando)), true, option::none(), + option::none()); let a = multi_action::get_authorities(new_resource_address); - assert!(vector::length(&a) == 2, 7357002); // bob votes and it becomes final. Bob could either use vote_governance() @@ -712,7 +717,7 @@ module ol_framework::test_multi_action { assert!(multi_action::is_authority(new_resource_address, signer::address_of(marlon_rando)), 7357004); // Now Rando and Bob, will conspire to remove alice. - // NOTE: false means remove here + // NOTE: `false` means `remove account` here let id = multi_action::propose_governance(marlon_rando, new_resource_address, vector::singleton(signer::address_of(alice)), false, option::none(), option::none()); let a = multi_action::get_authorities(new_resource_address); assert!(vector::length(&a) == 3, 7357002); // no change yet @@ -725,57 +730,61 @@ module ol_framework::test_multi_action { assert!(!multi_action::is_authority(new_resource_address, signer::address_of(alice)), 7357004); } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - fun governance_change_threshold(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - // Scenario: The multisig gets initiated with the 2 validators as the only authorities. IT takes 2-of-2 to sign. + // Happy day: change the threshold of a multisig + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun governance_change_threshold(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + // Scenario: The multisig gets initiated with the 2 bob and carol as the only authorities. It takes 2-of-2 to sign. // They decide next only 1-of-2 will be needed. - let vals = mock::genesis_n_vals(root, 2); + let _vals = mock::genesis_n_vals(root, 3); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - // Dave creates the resource account. HE is not one of the validators, and is not an authority in the multisig. - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); + + // Dave creates the resource account. He is not one of the validators, and is not an authority in the multisig. + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 7357001); // fund the account ol_account::transfer(alice, new_resource_address, 100); - // make the vals the signers on the safe - // SO ALICE and BOB ARE AUTHORIZED - multi_action::init_gov(&resource_sig);// both need to sign - multi_action::init_type(&resource_sig, false); // simple type with no capability - - //need to be caged to finalize multi action workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); - - // Alice is going to propose to change the authorities to add Rando - let id = multi_action::propose_governance(alice, new_resource_address, vector::empty(), true, option::some(1), option::none()); + // offer bob and carol authority on the safe + multi_action::init_gov(&resource_sig);// both need to sign + multi_action::init_type(&resource_sig, false); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(&resource_sig, authorities, option::none()); + multi_action::claim_offer(carol, new_resource_address); + multi_action::claim_offer(bob, new_resource_address); + multi_action::finalize_and_cage2(&resource_sig); + // carol is going to propose to change the authorities to add Rando + let id = multi_action::propose_governance(carol, new_resource_address, vector::empty(), true, option::some(1), option::none()); + + // check authorities and threshold let a = multi_action::get_authorities(new_resource_address); assert!(vector::length(&a) == 2, 7357002); // no change let (n, _m) = multi_action::get_threshold(new_resource_address); assert!(n == 2, 7357003); - // bob votes and it becomes final. Bob could either use vote_governance() let passed = multi_action::vote_governance(bob, new_resource_address, &id); + + // check authorities and threshold assert!(passed, 7357004); let a = multi_action::get_authorities(new_resource_address); assert!(vector::length(&a) == 2, 7357005); // no change let (n, _m) = multi_action::get_threshold(new_resource_address); - assert!(n == 1, 7357006); // now any other type of action can be taken with just one signer - let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - let id = multi_action::propose_new(bob, new_resource_address, proposal); - let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, new_resource_address); assert!(passed == true, 7357002); // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED + print(&cap_opt); assert!(option::is_none(&cap_opt), 7357003); option::destroy_none(cap_opt); diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 30971b549..62319bbda 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -64,7 +64,7 @@ module ol_framework::multi_action { /// Proposal is expired const EPROPOSAL_NOT_FOUND: u64 = 11; /// Proposal voting is closed - const EVOTING_CLOSED: u64 = 12; + const EVOTING_CLOSED: u64 = 0x12; /// No addresses in multisig changes const EEMPTY_ADDRESSES: u64 = 13; /// Duplicate vote @@ -104,13 +104,13 @@ module ol_framework::multi_action { /// DANGER - // Governance optionally holds a WithdrawCapability, which is used to withdraw funds from the account. All actions share the same WithdrawCapability. + /// Governance optionally holds a WithdrawCapability, which is used to withdraw funds from the account. All actions share the same WithdrawCapability. /// The WithdrawCapability can be used to withdraw funds from the account. /// Ordinarily only the signer/owner of this address can use it. /// We are bricking the signer, and as such the withdraw capability is now controlled by the Governance logic. /// Core Devs: This is a major attack vector. The WithdrawCapability should NEVER be returned to a public caller, UNLESS it is within the vote and approve flow. - /// Note, the WithdrawCApability is moved to this shared structure, and as such the signer of the account is bricked. The signer who was the original owner of this account ("sponsor") can no longer issue transactions to this account, and as such the WithdrawCapability would be inaccessible. So on initialization we extract the WithdrawCapability into the Governance governance struct. + /// Note, the WithdrawCapability is moved to this shared structure, and as such the signer of the account is bricked. The signer who was the original owner of this account ("sponsor") can no longer issue transactions to this account, and as such the WithdrawCapability would be inaccessible. So on initialization we extract the WithdrawCapability into the Governance governance struct. //TODO: feature: signers is a hashmap and each can have a different weight struct Governance has key { @@ -143,8 +143,8 @@ module ol_framework::multi_action { } /// Offer struct to manage the proposal and claiming of new authorities. - /// - proposed: List of addresses proposed for new authority roles. - /// - claimed: List of addresses that have claimed their proposed roles. + /// - proposed: List of authority addresses proposed + /// - claimed: List of authority addresses that have claimed the offer. /// - expiration_epoch: The epoch when the offer expires. struct Offer has key, store, drop { proposed: vector
, @@ -152,9 +152,36 @@ module ol_framework::multi_action { expiration_epoch: u64, } - // Proposes a new offer for authorities while the account is not yet initialized as multi_action. + // Initialize the governance structs for this account. + // Governance contains the constraints for each Action that are checked on each vote (n_sigs, expiration, signers, etc) + // Also, an initial Action of type PropGovSigners is created, which is used to govern the signers and threshold for this account. + public(friend) fun init_gov(sig: &signer) { + // heals un-initialized state, and does nothing if state already exists. + + let multisig_address = signer::address_of(sig); + // User footgun. The signer of this account is bricked, and as such the signer can no longer be an authority. + + if (!exists(multisig_address)) { + move_to(sig, Governance { + cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, + cfg_default_n_sigs: 0, // deprecate + signers: vector::empty(), + withdraw_capability: option::none(), + guid_capability: account::create_guid_capability(sig), + }); + }; + + if (!exists>(multisig_address)) { + move_to(sig, Action { + can_withdraw: false, + vote: ballot::new_tracker>(), + }); + }; + } + + // Proposes an offer for multisign authorities // - sig: The signer proposing the offer. - // - proposed: The list of addresses proposed for new authority roles. + // - proposed: The list of authorities addresses proposed. // - duration_epochs: The duration in epochs before the offer expires. public fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer{ let addr = signer::address_of(sig); @@ -307,34 +334,6 @@ module ol_framework::multi_action { assert!(is_authority(multisig_address, sender_addr), error::invalid_argument(ENOT_AUTHORIZED)); } - - // Initialize the governance structs for this account. - // Governance contains the constraints for each Action that are checked on each vote (n_sigs, expiration, signers, etc) - // Also, an initial Action of type PropGovSigners is created, which is used to govern the signers and threshold for this account. - public(friend) fun init_gov(sig: &signer) { - // heals un-initialized state, and does nothing if state already exists. - - let multisig_address = signer::address_of(sig); - // User footgun. The signer of this account is bricked, and as such the signer can no longer be an authority. - - if (!exists(multisig_address)) { - move_to(sig, Governance { - cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, - cfg_default_n_sigs: 0, // deprecate - signers: vector::empty(), - withdraw_capability: option::none(), - guid_capability: account::create_guid_capability(sig), - }); - }; - - if (!exists>(multisig_address)) { - move_to(sig, Action { - can_withdraw: false, - vote: ballot::new_tracker>(), - }); - }; - } - // TODO: Remove this public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { @@ -528,7 +527,7 @@ module ol_framework::multi_action { // does this proposal already exist in the pending list? let (found, _idx, status_enum, is_complete) = ballot::find_anywhere>(&action.vote, id); assert!(found, error::invalid_argument(EPROPOSAL_NOT_FOUND)); - assert!(status_enum == ballot::get_pending_enum(), error::invalid_argument(EVOTING_CLOSED)); + assert!(status_enum == ballot::get_pending_enum(), error::invalid_state(EVOTING_CLOSED)); assert!(!is_complete, error::invalid_argument(EVOTING_CLOSED)); let b = ballot::get_ballot_by_id_mut(&mut action.vote, id); From beb8e4be54a46d312293b14160cad87bdc2c2ec3 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Thu, 13 Jun 2024 20:19:40 -0300 Subject: [PATCH 09/68] adds offer flow to new authorities voted --- .../tests/vote_lib/multi_action.test.move | 95 +++++++++++-------- .../ol_sources/vote_lib/multi_action.move | 72 ++++++++++---- 2 files changed, 106 insertions(+), 61 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index e90187389..dba834740 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -12,7 +12,7 @@ module ol_framework::test_multi_action { use diem_framework::reconfiguration; use diem_framework::account; - use diem_std::debug::print; + // use diem_std::debug::print; struct DummyType has drop, store {} @@ -125,11 +125,12 @@ module ol_framework::test_multi_action { let claimed = vector::empty
(); vector::push_back(&mut claimed, signer::address_of(alice)); vector::push_back(&mut claimed, signer::address_of(bob)); - print(&authorities); assert!(authorities == claimed, 0); - // check offer was removed - assert!(!multi_action::exists_offer(carol_address), 0); + // check offer was cleaned + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 0); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 0); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 0); } // Propose another offer with different authorities @@ -149,7 +150,6 @@ module ol_framework::test_multi_action { // check new authorities assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 2, 0); - print(&multi_action::get_offer_proposed(@0x1000a)); assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 0); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 0); } @@ -667,7 +667,6 @@ module ol_framework::test_multi_action { mock::trigger_epoch(root); // epoch 4 -- now expired let epoch = reconfiguration::get_current_epoch(); - assert!(epoch == 4, 7357003); // trying to vote on a closed ballot will error @@ -675,59 +674,76 @@ module ol_framework::test_multi_action { } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, marlon_rando = @0x123456)] - fun governance_change_auths(root: &signer, alice: &signer, bob: &signer, carol: &signer, marlon_rando: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun governance_change_auths(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { // Scenario: The multisig gets initiated with the 2 validators as the only authorities. IT takes 2-of-2 to sign. - // later they add a third (Rando) so it becomes a 2-of-3. - // Rando and Bob, then remove alice so it becomes 2-of-2 again + // later they add a third (Dave) so it becomes a 2-of-3. + // Dave and Bob, then remove alice so it becomes 2-of-2 again - let _vals = mock::genesis_n_vals(root, 2); + let _vals = mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - // Dave creates the resource account. HE is not one of the validators, and is not an authority in the multisig. - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 7357001); - + let carol_address = @0x1000c; + let dave_address = @0x1000d; + // fund the account - ol_account::transfer(alice, new_resource_address, 100); + ol_account::transfer(alice, carol_address, 100); // offer alice and bob authority on the safe - multi_action::init_gov(&resource_sig);// both need to sign - multi_action::init_type(&resource_sig, true); + multi_action::init_gov(carol);// both need to sign + multi_action::init_type(carol, true); let authorities = vector::empty
(); vector::push_back(&mut authorities, signer::address_of(alice)); vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(&resource_sig, authorities, option::none()); - multi_action::claim_offer(alice, new_resource_address); - multi_action::claim_offer(bob, new_resource_address); - multi_action::finalize_and_cage2(&resource_sig); + multi_action::propose_offer(carol, authorities, option::none()); + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + multi_action::finalize_and_cage2(carol); - // alice is going to propose to change the authorities to add Rando - let id = multi_action::propose_governance(alice, new_resource_address, - vector::singleton(signer::address_of(marlon_rando)), true, option::none(), + // alice is going to propose to change the authorities to add dave + let id = multi_action::propose_governance(alice, carol_address, + vector::singleton(dave_address), true, option::none(), option::none()); - let a = multi_action::get_authorities(new_resource_address); - assert!(vector::length(&a) == 2, 7357002); + // check authorities did not change + let ret = multi_action::get_authorities(carol_address); + assert!(ret == authorities, 7357002); - // bob votes and it becomes final. Bob could either use vote_governance() - let passed = multi_action::vote_governance(bob, new_resource_address, &id); + // bob votes. bob could either use vote_governance() + let passed = multi_action::vote_governance(bob, carol_address, &id); assert!(passed, 7357003); - let a = multi_action::get_authorities(new_resource_address); - assert!(vector::length(&a) == 3, 7357003); - assert!(multi_action::is_authority(new_resource_address, signer::address_of(marlon_rando)), 7357004); - // Now Rando and Bob, will conspire to remove alice. + // check authorities did not change + let ret = multi_action::get_authorities(carol_address); + assert!(ret == authorities, 7357002); + + // check the offer + let ret = multi_action::get_offer_proposed(carol_address); + assert!(ret == vector::singleton(dave_address), 7357003); + + // dave claims the offer and it becomes final. + multi_action::claim_offer(dave, carol_address); + + // Chek new set of authorities + let ret = multi_action::get_authorities(carol_address); + vector::push_back(&mut authorities, dave_address); + assert!(ret == authorities, 7357003); + + // Check offer is cleaned up + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); + // assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 7357006); + + // Now dave and bob, will conspire to remove alice. // NOTE: `false` means `remove account` here - let id = multi_action::propose_governance(marlon_rando, new_resource_address, vector::singleton(signer::address_of(alice)), false, option::none(), option::none()); - let a = multi_action::get_authorities(new_resource_address); + let id = multi_action::propose_governance(dave, carol_address, vector::singleton(signer::address_of(alice)), false, option::none(), option::none()); + let a = multi_action::get_authorities(carol_address); assert!(vector::length(&a) == 3, 7357002); // no change yet // bob votes and it becomes final. Bob could either use vote_governance() - let passed = multi_action::vote_governance(bob, new_resource_address, &id); + let passed = multi_action::vote_governance(bob, carol_address, &id); assert!(passed, 7357003); - let a = multi_action::get_authorities(new_resource_address); + let a = multi_action::get_authorities(carol_address); assert!(vector::length(&a) == 2, 7357003); - assert!(!multi_action::is_authority(new_resource_address, signer::address_of(alice)), 7357004); + assert!(!multi_action::is_authority(carol_address, signer::address_of(alice)), 7357004); } // Happy day: change the threshold of a multisig @@ -784,7 +800,6 @@ module ol_framework::test_multi_action { assert!(passed == true, 7357002); // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED - print(&cap_opt); assert!(option::is_none(&cap_opt), 7357003); option::destroy_none(cap_opt); diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 62319bbda..b0a719c87 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -146,7 +146,7 @@ module ol_framework::multi_action { /// - proposed: List of authority addresses proposed /// - claimed: List of authority addresses that have claimed the offer. /// - expiration_epoch: The epoch when the offer expires. - struct Offer has key, store, drop { + struct Offer has key, store { proposed: vector
, claimed: vector
, expiration_epoch: u64, @@ -179,19 +179,13 @@ module ol_framework::multi_action { }; } - // Proposes an offer for multisign authorities - // - sig: The signer proposing the offer. - // - proposed: The list of authorities addresses proposed. - // - duration_epochs: The duration in epochs before the offer expires. - public fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer{ - let addr = signer::address_of(sig); - - // Ensure the account is not yet initialized as multisig - assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); - - // Ensure the account has governance initialized - assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + // Offer authorities voted. + fun propose_offer_voted(sig: &signer, multisign_address: address, proposed: vector
, duration_epochs: Option) acquires Offer { + // Propose the offer + propose_offer_address(sig, multisign_address, proposed, duration_epochs); + } + fun propose_offer_address(sig: &signer, addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); @@ -242,7 +236,6 @@ module ol_framework::multi_action { }; i = i + 1; }; - offer.proposed = proposed; offer.expiration_epoch = expiration_epoch; } else { @@ -256,10 +249,26 @@ module ol_framework::multi_action { } } + // Offer authorities for an account to be initialized as multisig. + // - sig: The signer proposing the offer. + // - proposed: The list of authorities addresses proposed. + // - duration_epochs: The duration in epochs before the offer expires. + public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + let addr = signer::address_of(sig); + + // Ensure the account is not yet initialized as multisig + assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); + + // Ensure the account has governance initialized + assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + + propose_offer_address(sig, addr, proposed, duration_epochs); + } + // Allows a proposed authority to claim their role. // - sig: The signer making the claim. // - multisig_address: The address of the multisig account. - public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer { + public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer, Governance { let sender_addr = signer::address_of(sig); // Ensure the account has an offer @@ -281,6 +290,21 @@ module ol_framework::multi_action { let (_, i) = vector::index_of(&offer.proposed, &sender_addr); vector::remove(&mut offer.proposed, i); vector::push_back(&mut offer.claimed, sender_addr); + + // if account is multisig, add authority to the multisig account + if (multisig_account::is_multisig(multisig_address)) { + let ms = borrow_global_mut(multisig_address); + maybe_update_authorities(ms, true, &vector::singleton(sender_addr)); + // remove sender_addr from offer claimed + let offer = borrow_global_mut(multisig_address); + let (_, i) = vector::index_of(&offer.claimed, &sender_addr); + vector::remove(&mut offer.claimed, i); + + if (vector::length(&offer.claimed) == 0 && vector::length(&offer.proposed) == 0) { + // clean expiration_epoch + offer.expiration_epoch = 0; + }; + }; } /// Finalizes the multisign account and locks it (cage). @@ -305,8 +329,11 @@ module ol_framework::multi_action { let initial_authorities = get_offer_claimed(addr); multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); - // remove offer - move_from(addr); + // clean offer + let offer = borrow_global_mut(addr); + offer.proposed = vector::empty(); + offer.claimed = vector::empty(); + offer.expiration_epoch = 0; } public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { @@ -427,7 +454,6 @@ module ol_framework::multi_action { }); } - fun maybe_extract_withdraw_cap(sig: &signer) acquires Governance { let multisig_address = signer::address_of(sig); assert!(exists(multisig_address), error::invalid_argument(ENOT_AUTHORIZED)); @@ -745,7 +771,7 @@ module ol_framework::multi_action { } // Proposing a governance change of adding or removing signer, or changing the n-of-m of the authorities. Note that proposing will deduplicate in the event that two authorities miscommunicate and send the same proposal, in that case for UX purposes the second proposal becomes a vote. - public(friend) fun propose_governance(sig: &signer, multisig_address: address, addresses: vector
, add_remove: bool, n_of_m: Option, duration_epochs: Option ): guid::ID acquires Governance, Action { + public(friend) fun propose_governance(sig: &signer, multisig_address: address, addresses: vector
, add_remove: bool, n_of_m: Option, duration_epochs: Option ): guid::ID acquires Governance, Action, Offer { assert_authorized(sig, multisig_address); // Duplicated with propose(), belt // and suspenders @@ -763,7 +789,7 @@ module ol_framework::multi_action { } /// This function can be called directly. Or the user can call propose_governance() with same parameters, which will deduplicate the proposal and instead vote. Voting is always a positive vote. There is no negative (reject) vote. - public(friend) fun vote_governance(sig: &signer, multisig_address: address, id: &guid::ID): bool acquires Governance, Action { + public(friend) fun vote_governance(sig: &signer, multisig_address: address, id: &guid::ID): bool acquires Governance, Action, Offer { assert_authorized(sig, multisig_address); let (passed, cap_opt) = { @@ -775,7 +801,11 @@ module ol_framework::multi_action { let ms = borrow_global_mut(multisig_address); let data = extract_proposal_data(multisig_address, id); if (!vector::is_empty(&data.addresses)) { - maybe_update_authorities(ms, data.add_remove, &data.addresses); + if (data.add_remove) { + propose_offer_voted(sig, multisig_address, data.addresses, option::none()); + } else { + maybe_update_authorities(ms, data.add_remove, &data.addresses); + }; }; maybe_update_threshold(ms, &data.n_of_m); }; From 6c4955f3b03ee585b458845e292467490dfa1847 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 14 Jun 2024 08:30:14 -0300 Subject: [PATCH 10/68] creates multisign signer to offer authorities triggered by vote, and drop offer after finalize and after last claim --- .../sources/create_signer.move | 1 + .../tests/vote_lib/multi_action.test.move | 14 +++----- .../ol_sources/vote_lib/multi_action.move | 36 +++++++++---------- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/framework/libra-framework/sources/create_signer.move b/framework/libra-framework/sources/create_signer.move index d49f38b9a..8ea011724 100644 --- a/framework/libra-framework/sources/create_signer.move +++ b/framework/libra-framework/sources/create_signer.move @@ -19,6 +19,7 @@ module diem_framework::create_signer { //////// 0L //////// friend ol_framework::fee_maker; friend ol_framework::epoch_boundary; + friend ol_framework::multi_action; public(friend) native fun create_signer(addr: address): signer; } diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index dba834740..10dbf9e33 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -127,10 +127,8 @@ module ol_framework::test_multi_action { vector::push_back(&mut claimed, signer::address_of(bob)); assert!(authorities == claimed, 0); - // check offer was cleaned - assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 0); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 0); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 0); + // check offer was dropped + assert!(!multi_action::exists_offer(carol_address), 0); } // Propose another offer with different authorities @@ -715,7 +713,7 @@ module ol_framework::test_multi_action { let ret = multi_action::get_authorities(carol_address); assert!(ret == authorities, 7357002); - // check the offer + // check the Offer let ret = multi_action::get_offer_proposed(carol_address); assert!(ret == vector::singleton(dave_address), 7357003); @@ -727,10 +725,8 @@ module ol_framework::test_multi_action { vector::push_back(&mut authorities, dave_address); assert!(ret == authorities, 7357003); - // Check offer is cleaned up - assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); - // assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 7357006); + // Check if offer was dropped + assert!(!multi_action::exists_offer(carol_address), 7357004); // Now dave and bob, will conspire to remove alice. // NOTE: `false` means `remove account` here diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index b0a719c87..7d5021029 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -28,6 +28,7 @@ module ol_framework::multi_action { use std::signer; use std::error; use std::guid; + use diem_framework::create_signer::create_signer; use diem_framework::account::{Self, WithdrawCapability}; use diem_framework::multisig_account; use ol_framework::ballot::{Self, BallotTracker}; @@ -146,7 +147,7 @@ module ol_framework::multi_action { /// - proposed: List of authority addresses proposed /// - claimed: List of authority addresses that have claimed the offer. /// - expiration_epoch: The epoch when the offer expires. - struct Offer has key, store { + struct Offer has key, store, drop { proposed: vector
, claimed: vector
, expiration_epoch: u64, @@ -179,12 +180,6 @@ module ol_framework::multi_action { }; } - // Offer authorities voted. - fun propose_offer_voted(sig: &signer, multisign_address: address, proposed: vector
, duration_epochs: Option) acquires Offer { - // Propose the offer - propose_offer_address(sig, multisign_address, proposed, duration_epochs); - } - fun propose_offer_address(sig: &signer, addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); @@ -212,7 +207,7 @@ module ol_framework::multi_action { let expiration_epoch = epoch_helper::get_current_epoch() + duration_epochs; // Update the offer if exists, otherwise create a new one - if (exists(addr)) { + if (exists_offer(addr)) { // Update offer let offer = borrow_global_mut(addr); @@ -246,7 +241,7 @@ module ol_framework::multi_action { expiration_epoch, }; move_to(sig, offer); - } + } } // Offer authorities for an account to be initialized as multisig. @@ -272,7 +267,7 @@ module ol_framework::multi_action { let sender_addr = signer::address_of(sig); // Ensure the account has an offer - assert!(exists(multisig_address), error::not_found(ENOT_OFFERED)); + assert!(exists_offer(multisig_address), error::not_found(ENOT_OFFERED)); let offer = borrow_global_mut(multisig_address); @@ -300,9 +295,9 @@ module ol_framework::multi_action { let (_, i) = vector::index_of(&offer.claimed, &sender_addr); vector::remove(&mut offer.claimed, i); - if (vector::length(&offer.claimed) == 0 && vector::length(&offer.proposed) == 0) { - // clean expiration_epoch - offer.expiration_epoch = 0; + if (vector::length(&offer.proposed) == 0) { + // drop empty Offer + move_from(multisig_address); }; }; } @@ -322,18 +317,15 @@ module ol_framework::multi_action { assert!(exists>(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); // check claimed authorities - assert!(exists(addr), error::not_found(ENOT_OFFERED)); + assert!(exists_offer(addr), error::not_found(ENOT_OFFERED)); assert!(has_enough_offer_claimed(addr), error::invalid_state(ENOT_ENOUGH_CLAIMED)); // finalize the account let initial_authorities = get_offer_claimed(addr); multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); - // clean offer - let offer = borrow_global_mut(addr); - offer.proposed = vector::empty(); - offer.claimed = vector::empty(); - offer.expiration_epoch = 0; + // drop the offer + move_from(addr); } public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { @@ -802,7 +794,11 @@ module ol_framework::multi_action { let data = extract_proposal_data(multisig_address, id); if (!vector::is_empty(&data.addresses)) { if (data.add_remove) { - propose_offer_voted(sig, multisig_address, data.addresses, option::none()); + // We create the signer for the multisig account here since this is required + // to add the Offer resource, specialy for pre existing multisig accounts. + // This should be safe because it is triggered by the vote governance. + let multisig_account = &create_signer(multisig_address); + propose_offer_address(multisig_account, multisig_address, data.addresses, option::none()); } else { maybe_update_authorities(ms, data.add_remove, &data.addresses); }; From e3f0a3ee1b820f6a376c4a6cfdb313086f99afa2 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 14 Jun 2024 08:33:54 -0300 Subject: [PATCH 11/68] add test finalize multisign account having a pending claim --- .../tests/vote_lib/multi_action.test.move | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 10dbf9e33..4e684cead 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -131,6 +131,44 @@ module ol_framework::test_multi_action { assert!(!multi_action::exists_offer(carol_address), 0); } + // Finalize multisign account having a pending claim + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d)] + fun finalize_with_pending_claim(root: &signer, carol: &signer, alice: &signer, bob: &signer, dave: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let carol_address = @0x1000c; + + // initialize the multi_action account + multi_action::init_gov(carol); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(dave)); + multi_action::propose_offer(carol, authorities, option::none()); + + // authorities claim the offer + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + + // finalize the multi_action account + assert!(account::exists_at(carol_address), 666); + multi_action::finalize_and_cage2(carol); + + // check the account is multi_action + assert!(multi_action::is_multi_action(carol_address), 0); + + // check authorities + let authorities = multi_action::get_authorities(carol_address); + let claimed = vector::empty
(); + vector::push_back(&mut claimed, signer::address_of(alice)); + vector::push_back(&mut claimed, signer::address_of(bob)); + assert!(authorities == claimed, 0); + + // check offer was dropped + assert!(!multi_action::exists_offer(carol_address), 0); + } + // Propose another offer with different authorities #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] fun propose_another_offer_different_authorities(root: &signer, alice: &signer) { From d729c986b568225de6daf35389a5687831e4ad97 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:11:28 -0300 Subject: [PATCH 12/68] adds migration to init offer, and never drop the offer updates tests error codes --- .../tests/vote_lib/multi_action.test.move | 149 ++++++++++-------- .../ol_sources/vote_lib/multi_action.move | 142 ++++++++++------- 2 files changed, 167 insertions(+), 124 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 4e684cead..57f934853 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -12,8 +12,6 @@ module ol_framework::test_multi_action { use diem_framework::reconfiguration; use diem_framework::account; - // use diem_std::debug::print; - struct DummyType has drop, store {} #[test(root = @ol_framework, carol = @0x1000c)] @@ -22,7 +20,7 @@ module ol_framework::test_multi_action { let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 0); + assert!(resource_account::is_resource_account(new_resource_address), 7357001); // make the vals the signers on the safe multi_action::init_gov(&resource_sig); @@ -36,8 +34,8 @@ module ol_framework::test_multi_action { let carol_address = @0x1000c; // check the offer does not exist - assert!(!multi_action::exists_offer(carol_address), 0); - assert!(!multi_action::is_multi_action(carol_address), 0); + assert!(!multi_action::exists_offer(carol_address), 7357001); + assert!(!multi_action::is_multi_action(carol_address), 7357002); // initialize the multi_action account multi_action::init_gov(carol); @@ -49,12 +47,12 @@ module ol_framework::test_multi_action { multi_action::propose_offer(carol, authorities, option::some(3)); // check the offer is proposed and account is not muti_action yet - assert!(multi_action::exists_offer(carol_address), 0); - assert!(multi_action::get_offer_proposed(carol_address) == authorities, 0); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 0); - assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 0); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 3, 0); - assert!(!multi_action::is_multi_action(carol_address), 0); + assert!(multi_action::exists_offer(carol_address), 7357003); + assert!(multi_action::get_offer_proposed(carol_address) == authorities, 7357004); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); + assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 7357006); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 3, 7357007); + assert!(!multi_action::is_multi_action(carol_address), 7357008); } // Happy Day: claim offer by authorities @@ -76,11 +74,11 @@ module ol_framework::test_multi_action { multi_action::claim_offer(bob, carol_address); // check the claimed offer - assert!(multi_action::exists_offer(carol_address), 0); + assert!(multi_action::exists_offer(carol_address), 7357001); let claimed = vector::singleton(signer::address_of(bob)); let proposed = vector::singleton(signer::address_of(alice)); - assert!(multi_action::get_offer_claimed(carol_address) == claimed, 0); - assert!(multi_action::get_offer_proposed(carol_address) == proposed, 0); + assert!(multi_action::get_offer_claimed(carol_address) == claimed, 7357002); + assert!(multi_action::get_offer_proposed(carol_address) == proposed, 7357003); // alice claim the offer multi_action::claim_offer(alice, carol_address); @@ -90,8 +88,8 @@ module ol_framework::test_multi_action { let authorities = vector::empty
(); vector::push_back(&mut authorities, signer::address_of(bob)); vector::push_back(&mut authorities, signer::address_of(alice)); - assert!(claimed == authorities, 0); - assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 0); + assert!(claimed == authorities, 7357004); + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357005); } // Happy Day: finalize multisign account @@ -114,21 +112,23 @@ module ol_framework::test_multi_action { multi_action::claim_offer(bob, carol_address); // finalize the multi_action account - assert!(account::exists_at(carol_address), 666); + assert!(account::exists_at(carol_address), 7357001); multi_action::finalize_and_cage2(carol); // check the account is multi_action - assert!(multi_action::is_multi_action(carol_address), 0); + assert!(multi_action::is_multi_action(carol_address), 7357002); // check authorities let authorities = multi_action::get_authorities(carol_address); let claimed = vector::empty
(); vector::push_back(&mut claimed, signer::address_of(alice)); vector::push_back(&mut claimed, signer::address_of(bob)); - assert!(authorities == claimed, 0); + assert!(authorities == claimed, 7357003); - // check offer was dropped - assert!(!multi_action::exists_offer(carol_address), 0); + // check offer was cleaned + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 7357006); } // Finalize multisign account having a pending claim @@ -152,21 +152,24 @@ module ol_framework::test_multi_action { multi_action::claim_offer(bob, carol_address); // finalize the multi_action account - assert!(account::exists_at(carol_address), 666); + assert!(account::exists_at(carol_address), 7357001); multi_action::finalize_and_cage2(carol); // check the account is multi_action - assert!(multi_action::is_multi_action(carol_address), 0); + assert!(multi_action::is_multi_action(carol_address), 7357002); // check authorities let authorities = multi_action::get_authorities(carol_address); let claimed = vector::empty
(); vector::push_back(&mut claimed, signer::address_of(alice)); vector::push_back(&mut claimed, signer::address_of(bob)); - assert!(authorities == claimed, 0); + assert!(authorities == claimed, 7357003); + + // check offer was cleared + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 7357006); - // check offer was dropped - assert!(!multi_action::exists_offer(carol_address), 0); } // Propose another offer with different authorities @@ -185,9 +188,9 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(2)); // check new authorities - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 2, 0); - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 0); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 0); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 2, 7357001); + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357002); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357003); } // Propose new offer with more authorities @@ -206,8 +209,8 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(2)); // check new authorities - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 0); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 0); + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); // carol claim the offer multi_action::claim_offer(carol, @0x1000a); @@ -220,9 +223,9 @@ module ol_framework::test_multi_action { let proposed = vector::empty(); vector::push_back(&mut proposed, @0x1000b); vector::push_back(&mut proposed, @0x1000d); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 3, 0); - assert!(multi_action::get_offer_proposed(@0x1000a) == proposed, 0); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 0); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 3, 7357003); + assert!(multi_action::get_offer_proposed(@0x1000a) == proposed, 7357004); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 7357005); } // Propose new offer with less authorities @@ -231,32 +234,32 @@ module ol_framework::test_multi_action { let _vals = mock::genesis_n_vals(root, 4); multi_action::init_gov(alice); - // invite bob, carol and dave + // invite bob, carol e dave let authorities = vector::empty
(); vector::push_back(&mut authorities, @0x1000b); vector::push_back(&mut authorities, @0x1000c); vector::push_back(&mut authorities, @0x1000d); multi_action::propose_offer(alice, authorities, option::some(2)); - // new invite bob and carol + // new invite bob e carol let new_authorities = vector::empty
(); vector::push_back(&mut new_authorities, @0x1000b); vector::push_back(&mut new_authorities, @0x1000c); multi_action::propose_offer(alice, new_authorities, option::some(3)); // check new authorities minus dave - assert!(multi_action::get_offer_proposed(@0x1000a) == new_authorities, 0); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 0); + assert!(multi_action::get_offer_proposed(@0x1000a) == new_authorities, 7357001); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); // carol claim the offer multi_action::claim_offer(carol, @0x1000a); - // new invite carol - multi_action::propose_offer(alice, vector::singleton(@0x1000c), option::some(4)); + // new invite bob only + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(4)); - // check new authorities minus bob - assert!(multi_action::get_offer_proposed(@0x1000a) == vector::empty(), 0); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 0); + // check new authorities minus carol + assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000b), 7357003); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357004); } // Propose new offer with same authorities @@ -265,28 +268,28 @@ module ol_framework::test_multi_action { let _vals = mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); - // invite bob and carol + // invite bob e carol let authorities = vector::empty
(); vector::push_back(&mut authorities, @0x1000b); vector::push_back(&mut authorities, @0x1000c); multi_action::propose_offer(alice, authorities, option::some(2)); - // new invite bob and carol + // new invite bob e carol multi_action::propose_offer(alice, authorities, option::some(3)); // check authorities - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 0); + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); // bob claim the offer multi_action::claim_offer(bob, @0x1000a); - // new invite bob and carol + // new invite bob e carol multi_action::propose_offer(alice, authorities, option::some(4)); // check authorities - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 4, 0); - assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000c), 0); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000b), 0); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 4, 7357002); + assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000c), 7357003); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000b), 7357004); } // Try to propose offer without governance @@ -446,7 +449,7 @@ module ol_framework::test_multi_action { // Try to finalize account without offer #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x60017, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x30018, location = ol_framework::multi_action)] fun finalize_without_offer(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 1); multi_action::init_gov(alice); @@ -524,7 +527,7 @@ module ol_framework::test_multi_action { // SHOULD NOT HAVE COUNTED ANY VOTES let v = multi_action::get_votes(alice_address, guid::id_creation_num(&id)); - assert!(vector::length(&v) == 0, 7357003); + assert!(vector::length(&v) == 0, 7357001); } // Multisign authorities bob and carol try to send the same proposal @@ -562,20 +565,19 @@ module ol_framework::test_multi_action { let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); multi_action::propose_new(carol, alice_address, proposal); let count = multi_action::get_count_of_pending(alice_address); - assert!(count == 1, 7357005); // no change + assert!(count == 1, 7357004); // no change // confirm there are no votes let v = multi_action::get_votes(alice_address, guid::id_creation_num(&id)); - assert!(vector::length(&v) == 0, 7357003); + assert!(vector::length(&v) == 0, 7357005); // proposing a different ending epoch will have no effect on the proposal once it is started. Here we try to set to epoch 4, but nothing should change, at it will still be 14. let proposal = multi_action::proposal_constructor(DummyType{}, option::some(4)); multi_action::propose_new(carol, alice_address, proposal); let count = multi_action::get_count_of_pending(alice_address); - assert!(count == 1, 7357004); + assert!(count == 1, 7357006); let epoch = multi_action::get_expiration(alice_address, guid::id_creation_num(&id)); - assert!(epoch != 4, 7357004); - assert!(epoch == 14, 7357005); + assert!(epoch == 14, 7357007); } // Happy day: complete vote action @@ -741,19 +743,19 @@ module ol_framework::test_multi_action { // check authorities did not change let ret = multi_action::get_authorities(carol_address); - assert!(ret == authorities, 7357002); + assert!(ret == authorities, 7357001); // bob votes. bob could either use vote_governance() let passed = multi_action::vote_governance(bob, carol_address, &id); - assert!(passed, 7357003); + assert!(passed, 7357002); // check authorities did not change let ret = multi_action::get_authorities(carol_address); - assert!(ret == authorities, 7357002); + assert!(ret == authorities, 7357003); // check the Offer let ret = multi_action::get_offer_proposed(carol_address); - assert!(ret == vector::singleton(dave_address), 7357003); + assert!(ret == vector::singleton(dave_address), 7357004); // dave claims the offer and it becomes final. multi_action::claim_offer(dave, carol_address); @@ -761,23 +763,30 @@ module ol_framework::test_multi_action { // Chek new set of authorities let ret = multi_action::get_authorities(carol_address); vector::push_back(&mut authorities, dave_address); - assert!(ret == authorities, 7357003); + assert!(ret == authorities, 7357005); - // Check if offer was dropped - assert!(!multi_action::exists_offer(carol_address), 7357004); + // Check if offer was cleaned + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357006); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357007); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 7357008); // Now dave and bob, will conspire to remove alice. // NOTE: `false` means `remove account` here let id = multi_action::propose_governance(dave, carol_address, vector::singleton(signer::address_of(alice)), false, option::none(), option::none()); let a = multi_action::get_authorities(carol_address); - assert!(vector::length(&a) == 3, 7357002); // no change yet + assert!(vector::length(&a) == 3, 7357009); // no change yet // bob votes and it becomes final. Bob could either use vote_governance() let passed = multi_action::vote_governance(bob, carol_address, &id); - assert!(passed, 7357003); + assert!(passed, 7357008); let a = multi_action::get_authorities(carol_address); - assert!(vector::length(&a) == 2, 7357003); - assert!(!multi_action::is_authority(carol_address, signer::address_of(alice)), 7357004); + assert!(vector::length(&a) == 2, 73570010); + assert!(!multi_action::is_authority(carol_address, signer::address_of(alice)), 7357011); + + // Check if offer was cleaned + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 73570012); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 73570013); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 73570014); } // Happy day: change the threshold of a multisig @@ -831,10 +840,10 @@ module ol_framework::test_multi_action { let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); let id = multi_action::propose_new(bob, new_resource_address, proposal); let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, new_resource_address); - assert!(passed == true, 7357002); + assert!(passed == true, 7357007); // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED - assert!(option::is_none(&cap_opt), 7357003); + assert!(option::is_none(&cap_opt), 7357008); option::destroy_none(cap_opt); } diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 7d5021029..d46ed1941 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -147,12 +147,27 @@ module ol_framework::multi_action { /// - proposed: List of authority addresses proposed /// - claimed: List of authority addresses that have claimed the offer. /// - expiration_epoch: The epoch when the offer expires. - struct Offer has key, store, drop { + struct Offer has key, store { proposed: vector
, claimed: vector
, expiration_epoch: u64, } + fun construct_empty_offer(): Offer { + Offer { + proposed: vector::empty(), + claimed: vector::empty(), + expiration_epoch: 0, + } + } + + fun clean_offer(addr: address) acquires Offer { + let offer = borrow_global_mut(addr); + offer.proposed = vector::empty(); + offer.claimed = vector::empty(); + offer.expiration_epoch = 0; + } + // Initialize the governance structs for this account. // Governance contains the constraints for each Action that are checked on each vote (n_sigs, expiration, signers, etc) // Also, an initial Action of type PropGovSigners is created, which is used to govern the signers and threshold for this account. @@ -178,9 +193,14 @@ module ol_framework::multi_action { vote: ballot::new_tracker>(), }); }; + + if (!exists(multisig_address)) { + move_to(sig, construct_empty_offer()); + }; } - fun propose_offer_address(sig: &signer, addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { + // Private function to assist offer proposal by entry function and governance vote + fun propose_offer_address(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); @@ -206,42 +226,61 @@ module ol_framework::multi_action { let expiration_epoch = epoch_helper::get_current_epoch() + duration_epochs; - // Update the offer if exists, otherwise create a new one - if (exists_offer(addr)) { - // Update offer - let offer = borrow_global_mut(addr); - - // Remove claimed addresses that are not in the new proposed list - let j = 0; - while (j < vector::length(&offer.claimed)) { - let claimed_addr = vector::borrow(&offer.claimed, j); - if (!vector::contains(&proposed, claimed_addr)) { - vector::remove(&mut offer.claimed, j); - } else { - j = j + 1; - }; - }; + // Update offer + let offer = borrow_global_mut(addr); + + // Remove claimed addresses that are not in the new proposed list + let j = 0; + while (j < vector::length(&offer.claimed)) { + let claimed_addr = vector::borrow(&offer.claimed, j); + if (!vector::contains(&proposed, claimed_addr)) { + vector::remove(&mut offer.claimed, j); + } else { + j = j + 1; + }; + }; - // Remove new proposed addresses that are already claimed - let i = 0; - while (i < vector::length(&proposed)) { - let proposed_addr = vector::borrow(&proposed, i); - if (vector::contains(&offer.claimed, proposed_addr)) { - vector::remove(&mut proposed, i); - }; - i = i + 1; + // Remove new proposed addresses that are already claimed + let i = 0; + while (i < vector::length(&proposed)) { + let proposed_addr = vector::borrow(&proposed, i); + if (vector::contains(&offer.claimed, proposed_addr)) { + vector::remove(&mut proposed, i); }; - offer.proposed = proposed; - offer.expiration_epoch = expiration_epoch; + i = i + 1; + }; + offer.proposed = proposed; + offer.expiration_epoch = expiration_epoch; + } + + // TODO: test this - WIP + // DANGER - may forge the signer of the multisig account is necessary here + // Migrate an account to have structure Offer in order to propose authorities changes + public entry fun migrate_init_offer(sig: &signer, multisig_address: address) { + // Ensure the account does not have Offer structure + assert!(!exists_offer(multisig_address), error::already_exists(666)); + + // if account is multisig, forge signer and add Offer to the multisig account + if (multisig_account::is_multisig(multisig_address)) { + // a) multisig account: ensure the signer is in the authorities list + let authorities = multisig_account::owners(multisig_address); + assert!(vector::contains(&authorities, &signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); + + // We create the signer for the multisig account here since this is required + // to add the Offer resource. + // This should be safe because we check that the signer is in the authorities list. + // Also, after all accounts are migrated this function will be deprecated. + let multisig_signer = &create_signer(multisig_address); // <<< DANGER + + // create Offer structure + let offer = construct_empty_offer(); + move_to(multisig_signer, offer); } else { - // create new offer - let offer = Offer { - proposed, - claimed: vector::empty
(), - expiration_epoch, - }; + // b) initiated account: ensure the account is initialized with governance and add Offer to the account + assert!(!is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); + let offer = construct_empty_offer(); move_to(sig, offer); - } + }; } // Offer authorities for an account to be initialized as multisig. @@ -249,18 +288,20 @@ module ol_framework::multi_action { // - proposed: The list of authorities addresses proposed. // - duration_epochs: The duration in epochs before the offer expires. public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + // Propose the offer on the signer's account let addr = signer::address_of(sig); // Ensure the account is not yet initialized as multisig assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); - // Ensure the account has governance initialized + // Ensure the account has governance initialized and offer structure assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + assert!(exists_offer(addr), error::not_found(ENOT_OFFERED)); - propose_offer_address(sig, addr, proposed, duration_epochs); + propose_offer_address(addr, proposed, duration_epochs); } - // Allows a proposed authority to claim their role. + // Allows a proposed authority to claim their offer. // - sig: The signer making the claim. // - multisig_address: The address of the multisig account. public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer, Governance { @@ -281,24 +322,21 @@ module ol_framework::multi_action { let current_epoch = epoch_helper::get_current_epoch(); assert!(offer.expiration_epoch > current_epoch, error::out_of_range(EOFFER_EXPIRED)); - // Remove the sender from the proposed list and add to the claimed list + // Remove the sender from the proposed list let (_, i) = vector::index_of(&offer.proposed, &sender_addr); vector::remove(&mut offer.proposed, i); - vector::push_back(&mut offer.claimed, sender_addr); - // if account is multisig, add authority to the multisig account if (multisig_account::is_multisig(multisig_address)) { + // a) finalized account: add authority to the multisig account let ms = borrow_global_mut(multisig_address); maybe_update_authorities(ms, true, &vector::singleton(sender_addr)); - // remove sender_addr from offer claimed - let offer = borrow_global_mut(multisig_address); - let (_, i) = vector::index_of(&offer.claimed, &sender_addr); - vector::remove(&mut offer.claimed, i); - if (vector::length(&offer.proposed) == 0) { - // drop empty Offer - move_from(multisig_address); + // clean the Offer + clean_offer(multisig_address); }; + } else { + // b) initiated account: add sender to the claimed list + vector::push_back(&mut offer.claimed, sender_addr); }; } @@ -324,8 +362,8 @@ module ol_framework::multi_action { let initial_authorities = get_offer_claimed(addr); multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); - // drop the offer - move_from(addr); + // clean offer + clean_offer(addr); } public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { @@ -794,11 +832,7 @@ module ol_framework::multi_action { let data = extract_proposal_data(multisig_address, id); if (!vector::is_empty(&data.addresses)) { if (data.add_remove) { - // We create the signer for the multisig account here since this is required - // to add the Offer resource, specialy for pre existing multisig accounts. - // This should be safe because it is triggered by the vote governance. - let multisig_account = &create_signer(multisig_address); - propose_offer_address(multisig_account, multisig_address, data.addresses, option::none()); + propose_offer_address(multisig_address, data.addresses, option::none()); } else { maybe_update_authorities(ms, data.add_remove, &data.addresses); }; From 38a1bf395bd7b79990c641c204464a2376b762b0 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:32:05 -0300 Subject: [PATCH 13/68] adds test for expired offer --- .../tests/vote_lib/multi_action.test.move | 31 +++++++++++++++++++ .../ol_sources/vote_lib/multi_action.move | 22 ++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 57f934853..acaf9f360 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -55,6 +55,37 @@ module ol_framework::test_multi_action { assert!(!multi_action::is_multi_action(carol_address), 7357008); } + // Propose new offer after expired + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun propose_offer_after_expired(root: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + + // initialize the multi_action account + multi_action::init_gov(carol); + + // offer to alice + multi_action::propose_offer(carol, vector::singleton(@0x1000a), option::some(2)); + + // check the offer is valid + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 2, 7357004); + + // wait for the offer to expire + mock::trigger_epoch(root); // epoch 1 valid + mock::trigger_epoch(root); // epoch 2 expired + assert!(multi_action::is_offer_expired(carol_address), 7357005); + + // propose a new offer to bob + let new_authorities = vector::empty
(); + vector::push_back(&mut new_authorities, @0x1000b); + multi_action::propose_offer(carol, new_authorities, option::some(3)); + + // check the new offer is proposed + assert!(multi_action::get_offer_proposed(carol_address) == vector::singleton(@0x1000b), 7357007); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357008); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == 5, 7357009); + } + // Happy Day: claim offer by authorities #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] fun claim_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index d46ed1941..4332316e3 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -199,8 +199,17 @@ module ol_framework::multi_action { }; } + fun lazy_clean_offer_expired(addr: address) acquires Offer { + if (is_offer_expired(addr)) { + clean_offer(addr); + }; + } + // Private function to assist offer proposal by entry function and governance vote fun propose_offer_address(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { + // Avoid renew expired offer + lazy_clean_offer_expired(addr); + // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); @@ -310,6 +319,9 @@ module ol_framework::multi_action { // Ensure the account has an offer assert!(exists_offer(multisig_address), error::not_found(ENOT_OFFERED)); + // Ensure the offer has not expired + assert!(!is_offer_expired(multisig_address), error::out_of_range(EOFFER_EXPIRED)); + let offer = borrow_global_mut(multisig_address); // Ensure the sender is not in the claimed list @@ -318,10 +330,6 @@ module ol_framework::multi_action { // Ensure the sender is in the proposed list assert!(vector::contains(&offer.proposed, &sender_addr), error::not_found(EADDRESS_NOT_PROPOSED)); - // Ensure the offer has not expired - let current_epoch = epoch_helper::get_current_epoch(); - assert!(offer.expiration_epoch > current_epoch, error::out_of_range(EOFFER_EXPIRED)); - // Remove the sender from the proposed list let (_, i) = vector::index_of(&offer.proposed, &sender_addr); vector::remove(&mut offer.proposed, i); @@ -453,6 +461,12 @@ module ol_framework::multi_action { vector::length(&claimed) >= MIN_OFFER_CLAIMS_TO_CAGE } + // Query if the offer has expired. + public fun is_offer_expired(multisig_address: address): bool acquires Offer { + let offer = borrow_global(multisig_address); + epoch_helper::get_current_epoch() >= offer.expiration_epoch + } + /// Has a multisig struct for a given action been created? public(friend) fun has_action(addr: address):bool { exists>(addr) From b84ecd1220b857e0ddbdb004f7edac0cfbe5240d Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:38:36 -0300 Subject: [PATCH 14/68] cleans proposed list only when offer is expired --- .../sources/ol_sources/vote_lib/multi_action.move | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 4332316e3..f6b496c3b 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -201,7 +201,8 @@ module ol_framework::multi_action { fun lazy_clean_offer_expired(addr: address) acquires Offer { if (is_offer_expired(addr)) { - clean_offer(addr); + let offer = borrow_global_mut(addr); + offer.proposed = vector::empty(); }; } From eddd10a668cc8681d56356f1d92e4a028c565fe4 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:58:10 -0300 Subject: [PATCH 15/68] adds support for epoch expiration per authority --- .../tests/vote_lib/multi_action.test.move | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index acaf9f360..483028e17 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -12,6 +12,9 @@ module ol_framework::test_multi_action { use diem_framework::reconfiguration; use diem_framework::account; + // print + // use std::debug::print; + struct DummyType has drop, store {} #[test(root = @ol_framework, carol = @0x1000c)] @@ -51,7 +54,10 @@ module ol_framework::test_multi_action { assert!(multi_action::get_offer_proposed(carol_address) == authorities, 7357004); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 7357006); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 3, 7357007); + let expiration = vector::empty(); + vector::push_back(&mut expiration, 3); + vector::push_back(&mut expiration, 3); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == expiration, 7357007); assert!(!multi_action::is_multi_action(carol_address), 7357008); } @@ -68,22 +74,20 @@ module ol_framework::test_multi_action { multi_action::propose_offer(carol, vector::singleton(@0x1000a), option::some(2)); // check the offer is valid - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 2, 7357004); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::singleton(2), 7357004); // wait for the offer to expire mock::trigger_epoch(root); // epoch 1 valid mock::trigger_epoch(root); // epoch 2 expired - assert!(multi_action::is_offer_expired(carol_address), 7357005); + assert!(multi_action::is_offer_expired(carol_address, @0x1000a), 7357005); // propose a new offer to bob - let new_authorities = vector::empty
(); - vector::push_back(&mut new_authorities, @0x1000b); - multi_action::propose_offer(carol, new_authorities, option::some(3)); + multi_action::propose_offer(carol, vector::singleton(@0x1000b), option::some(3)); // check the new offer is proposed assert!(multi_action::get_offer_proposed(carol_address) == vector::singleton(@0x1000b), 7357007); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357008); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 5, 7357009); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::singleton(5), 7357009); } // Happy Day: claim offer by authorities @@ -159,7 +163,7 @@ module ol_framework::test_multi_action { // check offer was cleaned assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 7357006); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357006); } // Finalize multisign account having a pending claim @@ -199,7 +203,7 @@ module ol_framework::test_multi_action { // check offer was cleared assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 7357006); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357006); } @@ -219,7 +223,9 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(2)); // check new authorities - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 2, 7357001); + let expiration = vector::singleton(2); + vector::push_back(&mut expiration, 2); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357001); assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357002); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357003); } @@ -239,9 +245,12 @@ module ol_framework::test_multi_action { vector::push_back(&mut authorities, @0x1000c); multi_action::propose_offer(alice, authorities, option::some(2)); - // check new authorities + // check offer assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); + let expiration = vector::singleton(2); + vector::push_back(&mut expiration, 2); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); // carol claim the offer multi_action::claim_offer(carol, @0x1000a); @@ -251,11 +260,12 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(3)); // check new authorities - let proposed = vector::empty(); - vector::push_back(&mut proposed, @0x1000b); + let proposed = vector::singleton(@0x1000b); vector::push_back(&mut proposed, @0x1000d); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 3, 7357003); + let expiration = vector::singleton(3); + vector::push_back(&mut expiration, 3); assert!(multi_action::get_offer_proposed(@0x1000a) == proposed, 7357004); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 7357005); } @@ -266,21 +276,22 @@ module ol_framework::test_multi_action { multi_action::init_gov(alice); // invite bob, carol e dave - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x1000b); + let authorities = vector::singleton(@0x1000b); vector::push_back(&mut authorities, @0x1000c); vector::push_back(&mut authorities, @0x1000d); multi_action::propose_offer(alice, authorities, option::some(2)); // new invite bob e carol - let new_authorities = vector::empty
(); - vector::push_back(&mut new_authorities, @0x1000b); + let new_authorities = vector::singleton(@0x1000b); vector::push_back(&mut new_authorities, @0x1000c); multi_action::propose_offer(alice, new_authorities, option::some(3)); // check new authorities minus dave assert!(multi_action::get_offer_proposed(@0x1000a) == new_authorities, 7357001); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); + let expiration = vector::singleton(3); + vector::push_back(&mut expiration, 3); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); // carol claim the offer multi_action::claim_offer(carol, @0x1000a); @@ -305,11 +316,22 @@ module ol_framework::test_multi_action { vector::push_back(&mut authorities, @0x1000c); multi_action::propose_offer(alice, authorities, option::some(2)); + // check offer + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); + let expiration = vector::singleton(2); + vector::push_back(&mut expiration, 2); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + // new invite bob e carol multi_action::propose_offer(alice, authorities, option::some(3)); - // check authorities + // check offer assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); + let expiration = vector::singleton(3); + vector::push_back(&mut expiration, 3); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); // bob claim the offer multi_action::claim_offer(bob, @0x1000a); @@ -318,7 +340,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(4)); // check authorities - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == 4, 7357002); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector::singleton(4), 7357002); assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000c), 7357003); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000b), 7357004); } @@ -799,7 +821,7 @@ module ol_framework::test_multi_action { // Check if offer was cleaned assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357006); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357007); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 7357008); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357008); // Now dave and bob, will conspire to remove alice. // NOTE: `false` means `remove account` here @@ -817,7 +839,7 @@ module ol_framework::test_multi_action { // Check if offer was cleaned assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 73570012); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 73570013); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == 0, 73570014); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 73570014); } // Happy day: change the threshold of a multisig From 2f38b789573a1a5d80fa2ec518afe69816cde88e Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:51:04 -0300 Subject: [PATCH 16/68] fixes adding to offer authorities voted in different votes before claims --- .../tests/vote_lib/multi_action.test.move | 59 ++++++++++++- .../ol_sources/vote_lib/multi_action.move | 86 +++++++++++++++---- 2 files changed, 125 insertions(+), 20 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 483028e17..e34356a2c 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -13,7 +13,7 @@ module ol_framework::test_multi_action { use diem_framework::account; // print - // use std::debug::print; + use std::debug::print; struct DummyType has drop, store {} @@ -900,4 +900,61 @@ module ol_framework::test_multi_action { option::destroy_none(cap_opt); } + + // Vote new athority before the previous one is claimed + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, erik = @0x1000e)] + fun governance_vote_before_claim(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 5); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add dave + let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), true, option::none(), option::none()); + + // bob votes and dave does not claims the offer + multi_action::vote_governance(bob, alice_address, &id); + + // check authorities and threshold + assert!(multi_action::get_authorities(alice_address) == authorities, 7357001); + let (n, _m) = multi_action::get_threshold(alice_address); + assert!(n == 2, 7357002); + + // check offer + print(&multi_action::get_offer_proposed(alice_address)); + assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357003); + print(&multi_action::get_offer_proposed(alice_address)); + assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000d), 7357004); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(7), 7357005); + + mock::trigger_epoch(root); // epoch 1 + + // bob is going to propose to change the authorities to add erik + let id = multi_action::propose_governance(bob, alice_address, vector::singleton(@0x1000e), true, option::none(), option::none()); + + // carol votes + multi_action::vote_governance(carol, alice_address, &id); + + // check authorities and threshold + assert!(multi_action::get_authorities(alice_address) == authorities, 7357001); + let (n, _m) = multi_action::get_threshold(alice_address); + assert!(n == 2, 7357002); + + // check offer + assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357003); + let proposed = vector::singleton(@0x1000d); + vector::push_back(&mut proposed, @0x1000e); + assert!(multi_action::get_offer_proposed(alice_address) == proposed, 7357004); + let expiration = vector::singleton(7); + vector::push_back(&mut expiration, 8); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == expiration, 7357005); + } } diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index f6b496c3b..081eb333c 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -34,6 +34,8 @@ module ol_framework::multi_action { use ol_framework::ballot::{Self, BallotTracker}; use ol_framework::epoch_helper; + // use diem_std::debug::print; + friend ol_framework::community_wallet_init; friend ol_framework::donor_voice_txs; friend ol_framework::safe; @@ -41,8 +43,6 @@ module ol_framework::multi_action { #[test_only] friend ol_framework::test_multi_action; - // use diem_std::debug::print; - const EGOV_NOT_INITIALIZED: u64 = 0x1; /// The owner of this account can't be an authority, since it will subsequently be bricked. The signer of this account is no longer useful. The account is now controlled by the Governance logic. const ESIGNER_CANT_BE_AUTHORITY: u64 = 0x2; @@ -146,18 +146,18 @@ module ol_framework::multi_action { /// Offer struct to manage the proposal and claiming of new authorities. /// - proposed: List of authority addresses proposed /// - claimed: List of authority addresses that have claimed the offer. - /// - expiration_epoch: The epoch when the offer expires. + /// - expiration_epoch: The epoch when each proposed expires. struct Offer has key, store { proposed: vector
, claimed: vector
, - expiration_epoch: u64, + expiration_epoch: vector, } fun construct_empty_offer(): Offer { Offer { proposed: vector::empty(), claimed: vector::empty(), - expiration_epoch: 0, + expiration_epoch: vector::empty(), } } @@ -165,7 +165,7 @@ module ol_framework::multi_action { let offer = borrow_global_mut(addr); offer.proposed = vector::empty(); offer.claimed = vector::empty(); - offer.expiration_epoch = 0; + offer.expiration_epoch = vector::empty(); } // Initialize the governance structs for this account. @@ -199,18 +199,34 @@ module ol_framework::multi_action { }; } - fun lazy_clean_offer_expired(addr: address) acquires Offer { + /*fun lazy_clean_offer_expired(addr: address) acquires Offer { if (is_offer_expired(addr)) { let offer = borrow_global_mut(addr); offer.proposed = vector::empty(); }; + }*/ + + // TODO + // propose offer add + // propose offer remove + // propose offer update + // update scenarios + + // Private function to assist governance vote + fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { + let offer = borrow_global_mut(addr); + let duration = epoch_helper::get_current_epoch() + DEFAULT_EPOCHS_OFFER_EXPIRE; + let i = 0; + while (i < vector::length(&proposed)) { + let addr = vector::borrow(&proposed, i); + vector::push_back(&mut offer.proposed, *addr); + vector::push_back(&mut offer.expiration_epoch, duration); + i = i + 1; + }; } // Private function to assist offer proposal by entry function and governance vote - fun propose_offer_address(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { - // Avoid renew expired offer - lazy_clean_offer_expired(addr); - + fun propose_offer_address(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); @@ -259,8 +275,34 @@ module ol_framework::multi_action { }; i = i + 1; }; - offer.proposed = proposed; - offer.expiration_epoch = expiration_epoch; + + // update proposed and expiration_epoch + let k = 0; + while (k < vector::length(&proposed)) { + // if already contains the address, update the expiration_epoch + let proposed_addr = vector::borrow(&proposed, k); + let (found, i) = vector::index_of(&offer.proposed, proposed_addr); + if (found) { + vector::remove(&mut offer.expiration_epoch, i); + vector::insert(&mut offer.expiration_epoch, i, expiration_epoch); + } else { + vector::push_back(&mut offer.proposed, *proposed_addr); + vector::push_back(&mut offer.expiration_epoch, expiration_epoch); + }; + k = k + 1; + }; + + // Remove old proposed addresses that are not in the new proposed list + let j = 0; + while (j < vector::length(&offer.proposed)) { + let proposed_addr = vector::borrow(&offer.proposed, j); + if (!vector::contains(&proposed, proposed_addr)) { + vector::remove(&mut offer.proposed, j); + vector::remove(&mut offer.expiration_epoch, j); + } else { + j = j + 1; + }; + }; } // TODO: test this - WIP @@ -293,6 +335,7 @@ module ol_framework::multi_action { }; } + // TODO: set proposed limit to avoid DoS attack // Offer authorities for an account to be initialized as multisig. // - sig: The signer proposing the offer. // - proposed: The list of authorities addresses proposed. @@ -321,7 +364,7 @@ module ol_framework::multi_action { assert!(exists_offer(multisig_address), error::not_found(ENOT_OFFERED)); // Ensure the offer has not expired - assert!(!is_offer_expired(multisig_address), error::out_of_range(EOFFER_EXPIRED)); + assert!(!is_offer_expired(multisig_address, sender_addr), error::out_of_range(EOFFER_EXPIRED)); let offer = borrow_global_mut(multisig_address); @@ -331,9 +374,10 @@ module ol_framework::multi_action { // Ensure the sender is in the proposed list assert!(vector::contains(&offer.proposed, &sender_addr), error::not_found(EADDRESS_NOT_PROPOSED)); - // Remove the sender from the proposed list + // Remove the sender from the proposed list and expiration_epoch let (_, i) = vector::index_of(&offer.proposed, &sender_addr); vector::remove(&mut offer.proposed, i); + vector::remove(&mut offer.expiration_epoch, i); if (multisig_account::is_multisig(multisig_address)) { // a) finalized account: add authority to the multisig account @@ -452,7 +496,7 @@ module ol_framework::multi_action { } // Query offer expiration epoch. - public fun get_offer_expiration_epoch(multisig_address: address): u64 acquires Offer { + public fun get_offer_expiration_epoch(multisig_address: address): vector acquires Offer { borrow_global(multisig_address).expiration_epoch } @@ -463,9 +507,11 @@ module ol_framework::multi_action { } // Query if the offer has expired. - public fun is_offer_expired(multisig_address: address): bool acquires Offer { + public fun is_offer_expired(multisig_address: address, authority_address: address): bool acquires Offer { let offer = borrow_global(multisig_address); - epoch_helper::get_current_epoch() >= offer.expiration_epoch + let (_, i) = vector::index_of(&offer.proposed, &authority_address); + let expiration_epoch = vector::borrow(&offer.expiration_epoch, i); + epoch_helper::get_current_epoch() >= *expiration_epoch } /// Has a multisig struct for a given action been created? @@ -815,6 +861,7 @@ module ol_framework::multi_action { n_of_m: Option, // Optionally change the n of m threshold. To only change the n_of_m threshold, an empty list of addresses is required. } + // TODO: check if the addresses are on chain, does not contain the multisig_address and the current authorities // Proposing a governance change of adding or removing signer, or changing the n-of-m of the authorities. Note that proposing will deduplicate in the event that two authorities miscommunicate and send the same proposal, in that case for UX purposes the second proposal becomes a vote. public(friend) fun propose_governance(sig: &signer, multisig_address: address, addresses: vector
, add_remove: bool, n_of_m: Option, duration_epochs: Option ): guid::ID acquires Governance, Action, Offer { assert_authorized(sig, multisig_address); // Duplicated with propose(), belt @@ -847,7 +894,8 @@ module ol_framework::multi_action { let data = extract_proposal_data(multisig_address, id); if (!vector::is_empty(&data.addresses)) { if (data.add_remove) { - propose_offer_address(multisig_address, data.addresses, option::none()); + // offer the authority adition voted to be claimed + add_offer_addresses(multisig_address, data.addresses); } else { maybe_update_authorities(ms, data.add_remove, &data.addresses); }; From 175010230ac255e895cc54f52d2a0a657c9e2a7f Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:58:16 -0300 Subject: [PATCH 17/68] clean up --- .../ol_sources/vote_lib/multi_action.move | 138 ++++++++---------- 1 file changed, 60 insertions(+), 78 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 081eb333c..e3eaa8693 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -199,19 +199,6 @@ module ol_framework::multi_action { }; } - /*fun lazy_clean_offer_expired(addr: address) acquires Offer { - if (is_offer_expired(addr)) { - let offer = borrow_global_mut(addr); - offer.proposed = vector::empty(); - }; - }*/ - - // TODO - // propose offer add - // propose offer remove - // propose offer update - // update scenarios - // Private function to assist governance vote fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { let offer = borrow_global_mut(addr); @@ -225,8 +212,52 @@ module ol_framework::multi_action { }; } - // Private function to assist offer proposal by entry function and governance vote - fun propose_offer_address(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { + // TODO: test this - WIP + // DANGER - may forge the signer of the multisig account is necessary here + // Migrate an account to have structure Offer in order to propose authorities changes + public entry fun migrate_init_offer(sig: &signer, multisig_address: address) { + // Ensure the account does not have Offer structure + assert!(!exists_offer(multisig_address), error::already_exists(666)); + + // if account is multisig, forge signer and add Offer to the multisig account + if (multisig_account::is_multisig(multisig_address)) { + // a) multisig account: ensure the signer is in the authorities list + let authorities = multisig_account::owners(multisig_address); + assert!(vector::contains(&authorities, &signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); + + // We create the signer for the multisig account here since this is required + // to add the Offer resource. + // This should be safe because we check that the signer is in the authorities list. + // Also, after all accounts are migrated this function will be deprecated. + let multisig_signer = &create_signer(multisig_address); // <<< DANGER + + // create Offer structure + let offer = construct_empty_offer(); + move_to(multisig_signer, offer); + } else { + // b) initiated account: ensure the account is initialized with governance and add Offer to the account + assert!(!is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); + let offer = construct_empty_offer(); + move_to(sig, offer); + }; + } + + // TODO: set proposed limit to avoid DoS attack + // Offer authorities for an account to be initialized as multisig. + // - sig: The signer proposing the offer. + // - proposed: The list of authorities addresses proposed. + // - duration_epochs: The duration in epochs before the offer expires. + public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + // Propose the offer on the signer's account + let addr = signer::address_of(sig); + + // Ensure the account is not yet initialized as multisig + assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); + + // Ensure the account has governance initialized and offer structure + assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + assert!(exists_offer(addr), error::not_found(ENOT_OFFERED)); + // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); @@ -276,7 +307,19 @@ module ol_framework::multi_action { i = i + 1; }; - // update proposed and expiration_epoch + // Remove old proposed addresses that are not in the new proposed list + let j = 0; + while (j < vector::length(&offer.proposed)) { + let proposed_addr = vector::borrow(&offer.proposed, j); + if (!vector::contains(&proposed, proposed_addr)) { + vector::remove(&mut offer.proposed, j); + vector::remove(&mut offer.expiration_epoch, j); + } else { + j = j + 1; + }; + }; + + // Update proposed and expiration epoch lists let k = 0; while (k < vector::length(&proposed)) { // if already contains the address, update the expiration_epoch @@ -290,68 +333,7 @@ module ol_framework::multi_action { vector::push_back(&mut offer.expiration_epoch, expiration_epoch); }; k = k + 1; - }; - - // Remove old proposed addresses that are not in the new proposed list - let j = 0; - while (j < vector::length(&offer.proposed)) { - let proposed_addr = vector::borrow(&offer.proposed, j); - if (!vector::contains(&proposed, proposed_addr)) { - vector::remove(&mut offer.proposed, j); - vector::remove(&mut offer.expiration_epoch, j); - } else { - j = j + 1; - }; - }; - } - - // TODO: test this - WIP - // DANGER - may forge the signer of the multisig account is necessary here - // Migrate an account to have structure Offer in order to propose authorities changes - public entry fun migrate_init_offer(sig: &signer, multisig_address: address) { - // Ensure the account does not have Offer structure - assert!(!exists_offer(multisig_address), error::already_exists(666)); - - // if account is multisig, forge signer and add Offer to the multisig account - if (multisig_account::is_multisig(multisig_address)) { - // a) multisig account: ensure the signer is in the authorities list - let authorities = multisig_account::owners(multisig_address); - assert!(vector::contains(&authorities, &signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); - - // We create the signer for the multisig account here since this is required - // to add the Offer resource. - // This should be safe because we check that the signer is in the authorities list. - // Also, after all accounts are migrated this function will be deprecated. - let multisig_signer = &create_signer(multisig_address); // <<< DANGER - - // create Offer structure - let offer = construct_empty_offer(); - move_to(multisig_signer, offer); - } else { - // b) initiated account: ensure the account is initialized with governance and add Offer to the account - assert!(!is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); - let offer = construct_empty_offer(); - move_to(sig, offer); - }; - } - - // TODO: set proposed limit to avoid DoS attack - // Offer authorities for an account to be initialized as multisig. - // - sig: The signer proposing the offer. - // - proposed: The list of authorities addresses proposed. - // - duration_epochs: The duration in epochs before the offer expires. - public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { - // Propose the offer on the signer's account - let addr = signer::address_of(sig); - - // Ensure the account is not yet initialized as multisig - assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); - - // Ensure the account has governance initialized and offer structure - assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); - assert!(exists_offer(addr), error::not_found(ENOT_OFFERED)); - - propose_offer_address(addr, proposed, duration_epochs); + }; } // Allows a proposed authority to claim their offer. From 80f52a55b4eb1b677d57a913e564a14fc1912e6f Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:11:58 -0300 Subject: [PATCH 18/68] set max number of offer addresses to avoid DoS --- .../tests/vote_lib/multi_action.test.move | 21 +++++++++++++++++++ .../ol_sources/vote_lib/multi_action.move | 7 +++++++ 2 files changed, 28 insertions(+) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index e34356a2c..240c64425 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -387,6 +387,27 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, vector::empty
(), option::none()); } + // Try to propose too many authorities + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x10024, location = ol_framework::multi_action)] + fun propose_too_many_authorities(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x10001); + vector::push_back(&mut authorities, @0x10002); + vector::push_back(&mut authorities, @0x10003); + vector::push_back(&mut authorities, @0x10004); + vector::push_back(&mut authorities, @0x10005); + vector::push_back(&mut authorities, @0x10006); + vector::push_back(&mut authorities, @0x10007); + vector::push_back(&mut authorities, @0x10008); + vector::push_back(&mut authorities, @0x10009); + vector::push_back(&mut authorities, @0x10010); + vector::push_back(&mut authorities, @0x10011); + multi_action::propose_offer(alice, authorities, option::none()); + } + // Try to propose offer to the signer address #[test(root = @ol_framework, alice = @0x1000a)] #[expected_failure(abort_code = 0x50002, location = ol_framework::multi_action)] diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index e3eaa8693..b0a466327 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -88,6 +88,8 @@ module ol_framework::multi_action { const EZERO_DURATION: u64 = 0x22; /// Offer already claimed const EALREADY_CLAIMED: u64 = 0x23; + /// Too many addresses in offer - avoid DoS attack + const ETOO_MANY_ADDRESSES: u64 = 0x24; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; @@ -95,6 +97,8 @@ module ol_framework::multi_action { const DEFAULT_EPOCHS_OFFER_EXPIRE: u64 = 7; /// minimum number of claimed authorities to cage the account const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 2; + /// maximum number of address to offer + const MAX_OFFER_ADDRESSES: u64 = 10; /// A Governance account is an account which requires multiple votes from Authorities to send a transaction. /// A multisig can be used to get agreement on different types of Actions, such as a payment transaction where the handler code for the transaction is an a separate contract. See for example MultiSigPayment. @@ -261,6 +265,9 @@ module ol_framework::multi_action { // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); + // Ensure the proposed list is not greater than the maximum limit - avoid DoS attack + assert!(vector::length(&proposed) <= MAX_OFFER_ADDRESSES, error::invalid_argument(ETOO_MANY_ADDRESSES)); + // Ensure the proposed list does not contain the signer assert!(!vector::contains(&proposed, &addr), error::permission_denied(ESIGNER_CANT_BE_AUTHORITY)); From 0d5e57de549491540030730b1c7346dce3a5a797 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:30:20 -0300 Subject: [PATCH 19/68] test default value of epochs to expire --- .../ol_sources/tests/vote_lib/multi_action.test.move | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 240c64425..20a4e6bc7 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -47,7 +47,7 @@ module ol_framework::test_multi_action { let authorities = vector::empty
(); vector::push_back(&mut authorities, signer::address_of(alice)); vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::some(3)); + multi_action::propose_offer(carol, authorities, option::none()); // check the offer is proposed and account is not muti_action yet assert!(multi_action::exists_offer(carol_address), 7357003); @@ -55,8 +55,8 @@ module ol_framework::test_multi_action { assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 7357006); let expiration = vector::empty(); - vector::push_back(&mut expiration, 3); - vector::push_back(&mut expiration, 3); + vector::push_back(&mut expiration, 7); + vector::push_back(&mut expiration, 7); assert!(multi_action::get_offer_expiration_epoch(carol_address) == expiration, 7357007); assert!(!multi_action::is_multi_action(carol_address), 7357008); } From ffe0f7ccc65819e0d0ed00cf72ddb5cff5183dbd Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:18:59 -0300 Subject: [PATCH 20/68] ensure proposed list is distinct addresses and without owner --- .../tests/vote_lib/multi_action.test.move | 17 +++++++++++++++-- .../ol_sources/vote_lib/multi_action.move | 6 +++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 20a4e6bc7..25d391ec8 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -410,14 +410,27 @@ module ol_framework::test_multi_action { // Try to propose offer to the signer address #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x50002, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x10013, location = ol_framework::multisig_account)] fun propose_offer_to_signer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 4); + let _vals = mock::genesis_n_vals(root, 1); let alice_address = signer::address_of(alice); multi_action::init_gov(alice); multi_action::propose_offer(alice, vector::singleton
(alice_address), option::none()); } + // Try to propose offer with duplicated addresses + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x10001, location = ol_framework::multisig_account)] + fun propose_offer_duplicated_authorities(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + multi_action::init_gov(alice); + + let authorities = vector::singleton
(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000b); + multi_action::propose_offer(alice, authorities, option::none()); + } + // Try to propose offer to an invalid signer #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x60021, location = ol_framework::multi_action)] diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index b0a466327..965bb6747 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -267,9 +267,9 @@ module ol_framework::multi_action { // Ensure the proposed list is not greater than the maximum limit - avoid DoS attack assert!(vector::length(&proposed) <= MAX_OFFER_ADDRESSES, error::invalid_argument(ETOO_MANY_ADDRESSES)); - - // Ensure the proposed list does not contain the signer - assert!(!vector::contains(&proposed, &addr), error::permission_denied(ESIGNER_CANT_BE_AUTHORITY)); + + // Ensure distinct addresses and multisign owner not in the list + multisig_account::validate_owners(&proposed, addr); // Ensure the proposed list address are valid let i = 0; From 28bb4fa53c4a806dc0951faeb28e7c85bfc4bab3 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 15:39:13 -0300 Subject: [PATCH 21/68] adds tests for offer migration --- .../sources/multisig_account.move | 6 +- .../tests/vote_lib/multi_action.test.move | 102 ++++++++++++++++++ .../ol_sources/vote_lib/multi_action.move | 61 +++++++++-- 3 files changed, 157 insertions(+), 12 deletions(-) diff --git a/framework/libra-framework/sources/multisig_account.move b/framework/libra-framework/sources/multisig_account.move index 13d26c407..b161219d1 100644 --- a/framework/libra-framework/sources/multisig_account.move +++ b/framework/libra-framework/sources/multisig_account.move @@ -64,7 +64,7 @@ module diem_framework::multisig_account { // Any error codes > 2000 can be thrown as part of transaction prologue. /// Owner list cannot contain the same address more than once. - const EDUPLICATE_OWNER: u64 = 1; + const EDUPLICATE_OWNER: u64 = 0x1; /// Specified account is not a multisig account. const EACCOUNT_NOT_MULTISIG: u64 = 2002; /// Account executing this operation is not an owner of the multisig account. @@ -86,7 +86,7 @@ module diem_framework::multisig_account { /// Payload hash must be exactly 32 bytes (sha3-256). const EINVALID_PAYLOAD_HASH: u64 = 12; /// The multisig account itself cannot be an owner. - const EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF: u64 = 13; + const EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF: u64 = 0x13; /// Multisig accounts has not been enabled on this current network yet. const EMULTISIG_ACCOUNTS_NOT_ENABLED_YET: u64 = 14; /// The number of metadata keys and values don't match. @@ -1014,7 +1014,7 @@ module diem_framework::multisig_account { multisig_account_seed } - fun validate_owners(owners: &vector
, multisig_account: address) { + public(friend) fun validate_owners(owners: &vector
, multisig_account: address) { let distinct_owners: vector
= vector[]; vector::for_each_ref(owners, |owner| { let owner = *owner; diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 25d391ec8..0aa67f372 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -586,6 +586,108 @@ module ol_framework::test_multi_action { multi_action::finalize_and_cage2(alice); multi_action::finalize_and_cage2(alice); } + + // Offer Migration tests + + // Happy Day: init offer on a legacy multisign initiated account + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + fun migrate_offer_account_initialized(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov_deprecated(alice); + assert!(multi_action::exists_offer(alice_address) == false, 7357001); + + // migrate the offer + multi_action::migrate_offer(alice, alice_address); + assert!(multi_action::exists_offer(alice_address) == true, 7357002); + + // offer authority to bob + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(9)); + + // check the offer is proposed + assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000b), 7357003); + assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357004); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(9), 7357005); + } + + // Happy Day: init offer on a legacy mulstisign finalized account + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun migrate_offer_account_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // make alice account multisign with deprecated methods + multi_action::init_gov_deprecated(alice); + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + multi_action::finalize_and_cage_deprecated(alice, authorities, 2); + assert!(multi_action::exists_offer(alice_address) == false, 7357001); + + // carol migrate the offer + multi_action::migrate_offer(carol, alice_address); + assert!(multi_action::exists_offer(alice_address) == true, 7357002); + + // carol propose dave as new authority + let id = multi_action::propose_governance(carol, alice_address, + vector::singleton(@0x1000d), true, option::none(), + option::none()); + + // bob votes + let passed = multi_action::vote_governance(bob, alice_address, &id); + assert!(passed, 7357003); + + // dave claim the offer + multi_action::claim_offer(dave, alice_address); + + // check new authorities + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + assert!(multi_action::get_authorities(alice_address) == authorities, 7357004); + } + + // Try to migrate offer of an account already with offer + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x80025, location = ol_framework::multi_action)] + fun migrate_offer_account_with_offer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov(alice); + + // try to migrate offer + multi_action::migrate_offer(alice, alice_address); + } + + // Try to migrate offer of an account without governance initiated + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] + fun migrate_offer_account_without_gov(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + let alice_address = signer::address_of(alice); + + // try to migrate offer + multi_action::migrate_offer(alice, alice_address); + } + + // Try to migrate offer through someone without authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x50003, location = ol_framework::multi_action)] + fun migrate_offer_without_authority(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov_deprecated(alice); + + // try to migrate offer + multi_action::migrate_offer(bob, alice_address); + } + + // Governance Tests // Happy Day: propose a new action and check zero votes #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 965bb6747..36b50d735 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -47,7 +47,7 @@ module ol_framework::multi_action { /// The owner of this account can't be an authority, since it will subsequently be bricked. The signer of this account is no longer useful. The account is now controlled by the Governance logic. const ESIGNER_CANT_BE_AUTHORITY: u64 = 0x2; /// signer not authorized to approve a transaction. - const ENOT_AUTHORIZED: u64 = 3; + const ENOT_AUTHORIZED: u64 = 0x3; /// There are no pending transactions to search const EPENDING_EMPTY: u64 = 4; /// Not enough signers configured @@ -90,6 +90,8 @@ module ol_framework::multi_action { const EALREADY_CLAIMED: u64 = 0x23; /// Too many addresses in offer - avoid DoS attack const ETOO_MANY_ADDRESSES: u64 = 0x24; + /// Offer already exists + const EOFFER_ALREADY_EXISTS: u64 = 0x25; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; @@ -203,6 +205,30 @@ module ol_framework::multi_action { }; } + // TODO: remove this after offer migration is completed + #[test_only] + public(friend) fun init_gov_deprecated(sig: &signer) { + let multisig_address = signer::address_of(sig); + + if (!exists(multisig_address)) { + move_to(sig, Governance { + cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, + cfg_default_n_sigs: 0, // deprecate + signers: vector::empty(), + withdraw_capability: option::none(), + guid_capability: account::create_guid_capability(sig), + }); + }; + + if (!exists>(multisig_address)) { + move_to(sig, Action { + can_withdraw: false, + vote: ballot::new_tracker>(), + }); + }; + } + + // Private function to assist governance vote fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { let offer = borrow_global_mut(addr); @@ -216,12 +242,12 @@ module ol_framework::multi_action { }; } - // TODO: test this - WIP // DANGER - may forge the signer of the multisig account is necessary here - // Migrate an account to have structure Offer in order to propose authorities changes - public entry fun migrate_init_offer(sig: &signer, multisig_address: address) { + // TODO: remove this function after offer migration is completed + // Migrate a legacy account to have structure Offer in order to propose authorities changes + public entry fun migrate_offer(sig: &signer, multisig_address: address) { // Ensure the account does not have Offer structure - assert!(!exists_offer(multisig_address), error::already_exists(666)); + assert!(!exists_offer(multisig_address), error::already_exists(EOFFER_ALREADY_EXISTS)); // if account is multisig, forge signer and add Offer to the multisig account if (multisig_account::is_multisig(multisig_address)) { @@ -240,14 +266,15 @@ module ol_framework::multi_action { move_to(multisig_signer, offer); } else { // b) initiated account: ensure the account is initialized with governance and add Offer to the account - assert!(!is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); + assert!(multisig_address == signer::address_of(sig), error::permission_denied(ENOT_AUTHORIZED)); + assert!(is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); let offer = construct_empty_offer(); move_to(sig, offer); }; } - // TODO: set proposed limit to avoid DoS attack - // Offer authorities for an account to be initialized as multisig. + // Propose an offer to new authorities on the signer account + // or update the expiration epoch of the existing proposed authorities. // - sig: The signer proposing the offer. // - proposed: The list of authorities addresses proposed. // - duration_epochs: The duration in epochs before the offer expires. @@ -433,7 +460,23 @@ module ol_framework::multi_action { assert!(is_authority(multisig_address, sender_addr), error::invalid_argument(ENOT_AUTHORIZED)); } - // TODO: Remove this + // TODO: remove this function after offer migration is completed + #[test_only] + public entry fun finalize_and_cage_deprecated(sig: &signer, initial_authorities: + vector
, num_signers: u64) { + let addr = signer::address_of(sig); + assert!(exists(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + // not yet initialized + assert!(!multisig_account::is_multisig(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + + multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); + } + + // TODO: remove this function after dependencies are updated public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { let addr = signer::address_of(sig); From e760ce22d47b51e365edf065aaa97ea18dd3ddec Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:10:03 -0300 Subject: [PATCH 22/68] ensures gov proposes cannot have invalid, duplicated and owner address --- .../sources/multisig_account.move | 3 + .../tests/vote_lib/multi_action.test.move | 72 ++++++++++++++++++- .../ol_sources/vote_lib/multi_action.move | 6 +- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/framework/libra-framework/sources/multisig_account.move b/framework/libra-framework/sources/multisig_account.move index b161219d1..3c6b4911a 100644 --- a/framework/libra-framework/sources/multisig_account.move +++ b/framework/libra-framework/sources/multisig_account.move @@ -95,6 +95,8 @@ module diem_framework::multisig_account { const EDUPLICATE_METADATA_KEY: u64 = 16; /// The sequence number provided is invalid. It must be between [1, next pending transaction - 1]. const EINVALID_SEQUENCE_NUMBER: u64 = 17; + /// The owner does not exist in the chain. + const EOWNER_DOES_NOT_EXIST: u64 = 0x18; /// Represents a multisig account's configurations and transactions. /// This will be stored in the multisig account (created as a resource account separate from any owner accounts). @@ -1021,6 +1023,7 @@ module diem_framework::multisig_account { assert!(owner != multisig_account, error::invalid_argument(EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF)); let (found, _) = vector::index_of(&distinct_owners, &owner); assert!(!found, error::invalid_argument(EDUPLICATE_OWNER)); + assert!(account::exists_at(owner), error::not_found(EOWNER_DOES_NOT_EXIST)); vector::push_back(&mut distinct_owners, owner); }); } diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 0aa67f372..45b7665ae 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -433,7 +433,7 @@ module ol_framework::test_multi_action { // Try to propose offer to an invalid signer #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x60021, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x60018, location = ol_framework::multisig_account)] fun offer_to_invalid_authority(root: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); @@ -615,7 +615,7 @@ module ol_framework::test_multi_action { // Happy Day: init offer on a legacy mulstisign finalized account #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] fun migrate_offer_account_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { - let _vals = mock::genesis_n_vals(root, 2); + let _vals = mock::genesis_n_vals(root, 4); let alice_address = signer::address_of(alice); // make alice account multisign with deprecated methods @@ -1093,4 +1093,72 @@ module ol_framework::test_multi_action { vector::push_back(&mut expiration, 8); assert!(multi_action::get_offer_expiration_epoch(alice_address) == expiration, 7357005); } + + // Try to vote an invalid address for new authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x60018, location = ol_framework::multisig_account)] + fun governance_vote_invalid_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add dave + let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0xCAFE), true, option::none(), option::none()); + + // bob votes and dave does not claims the offer + multi_action::vote_governance(bob, alice_address, &id); + } + + // Try to vote duplicated addresses for new authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x10001, location = ol_framework::multisig_account)] + fun governance_vote_duplicated_addresses(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 5); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add dave twice + let authorities = vector::singleton(@0x1000d); + vector::push_back(&mut authorities, @0x1000d); + let _id = multi_action::propose_governance(carol, alice_address, authorities, true, option::none(), option::none()); + } + + // Try to vote multisig account address for new authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x10013, location = ol_framework::multisig_account)] + fun governance_vote_multisig_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 5); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add dave twice + let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(alice_address), true, option::none(), option::none()); + } } diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 36b50d735..9c28c7b8a 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -252,8 +252,7 @@ module ol_framework::multi_action { // if account is multisig, forge signer and add Offer to the multisig account if (multisig_account::is_multisig(multisig_address)) { // a) multisig account: ensure the signer is in the authorities list - let authorities = multisig_account::owners(multisig_address); - assert!(vector::contains(&authorities, &signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); + assert!(is_authority(multisig_address, signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); // We create the signer for the multisig account here since this is required // to add the Offer resource. @@ -899,6 +898,8 @@ module ol_framework::multi_action { assert_authorized(sig, multisig_address); // Duplicated with propose(), belt // and suspenders + multisig_account::validate_owners(&addresses, multisig_address); + let data = PropGovSigners { addresses, add_remove, @@ -928,6 +929,7 @@ module ol_framework::multi_action { if (data.add_remove) { // offer the authority adition voted to be claimed add_offer_addresses(multisig_address, data.addresses); + return passed } else { maybe_update_authorities(ms, data.add_remove, &data.addresses); }; From 587146dfeb1091cca1bc69b9df80daaa1524ef64 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:26:29 -0300 Subject: [PATCH 23/68] adds new constraints and tests to multi_action governance --- .../tests/vote_lib/multi_action.test.move | 42 +++++++++++++++++++ .../ol_sources/vote_lib/multi_action.move | 25 ++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 45b7665ae..9a2bec7ce 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -1161,4 +1161,46 @@ module ol_framework::test_multi_action { // carol is going to propose to change the authorities to add dave twice let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(alice_address), true, option::none(), option::none()); } + + // Try to vote an owner as new authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x10026, location = ol_framework::multi_action)] + fun governance_vote_owner_as_new_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add bob + let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(signer::address_of(bob)), true, option::none(), option::none()); + } + + // Try to vote remove an authority that is not in the multisig + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x60027, location = ol_framework::multi_action)] + fun governance_vote_remove_non_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to remove dave + let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), false, option::none(), option::none()); + } } diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 9c28c7b8a..c71f7fa25 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -92,6 +92,10 @@ module ol_framework::multi_action { const ETOO_MANY_ADDRESSES: u64 = 0x24; /// Offer already exists const EOFFER_ALREADY_EXISTS: u64 = 0x25; + /// Already an owner + const EALREADY_OWNER: u64 = 0x26; + /// Owner not found + const EOWNER_NOT_FOUND: u64 = 0x27; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; @@ -898,7 +902,7 @@ module ol_framework::multi_action { assert_authorized(sig, multisig_address); // Duplicated with propose(), belt // and suspenders - multisig_account::validate_owners(&addresses, multisig_address); + validate_owners(&addresses, multisig_address, add_remove); let data = PropGovSigners { addresses, @@ -954,6 +958,25 @@ module ol_framework::multi_action { }; } + fun validate_owners(addresses: &vector
, multisig_address: address, add_remove: bool) { + let auths = multisig_account::owners(multisig_address); + let i = 0; + if (add_remove) { + while (i < vector::length(addresses)) { + let addr = vector::borrow(addresses, i); + assert!(!vector::contains(&auths, addr), error::invalid_argument(EALREADY_OWNER)); + i = i + 1; + }; + } else { + while (i < vector::length(addresses)) { + let addr = vector::borrow(addresses, i); + assert!(vector::contains(&auths, addr), error::not_found(EOWNER_NOT_FOUND)); + i = i + 1; + }; + }; + multisig_account::validate_owners(addresses, multisig_address); + } + //////// GETTERS //////// #[view] public fun get_authorities(multisig_address: address): vector
{ From 0104e9574ab868cac9cd9ce5d09c9a7f2fb91a0a Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:13:37 -0300 Subject: [PATCH 24/68] refactoring --- .../sources/multisig_account.move | 2 + .../ol_sources/vote_lib/multi_action.move | 133 ++++++++++-------- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/framework/libra-framework/sources/multisig_account.move b/framework/libra-framework/sources/multisig_account.move index 3c6b4911a..5f9a19645 100644 --- a/framework/libra-framework/sources/multisig_account.move +++ b/framework/libra-framework/sources/multisig_account.move @@ -1016,6 +1016,8 @@ module diem_framework::multisig_account { multisig_account_seed } + /// Validate the owners list for a multisig account. + /// It is friend to ensure that multi_action use the same validation logic. public(friend) fun validate_owners(owners: &vector
, multisig_account: address) { let distinct_owners: vector
= vector[]; vector::for_each_ref(owners, |owner| { diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index c71f7fa25..db2a7316a 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -209,30 +209,6 @@ module ol_framework::multi_action { }; } - // TODO: remove this after offer migration is completed - #[test_only] - public(friend) fun init_gov_deprecated(sig: &signer) { - let multisig_address = signer::address_of(sig); - - if (!exists(multisig_address)) { - move_to(sig, Governance { - cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, - cfg_default_n_sigs: 0, // deprecate - signers: vector::empty(), - withdraw_capability: option::none(), - guid_capability: account::create_guid_capability(sig), - }); - }; - - if (!exists>(multisig_address)) { - move_to(sig, Action { - can_withdraw: false, - vote: ballot::new_tracker>(), - }); - }; - } - - // Private function to assist governance vote fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { let offer = borrow_global_mut(addr); @@ -276,21 +252,16 @@ module ol_framework::multi_action { }; } - // Propose an offer to new authorities on the signer account - // or update the expiration epoch of the existing proposed authorities. - // - sig: The signer proposing the offer. - // - proposed: The list of authorities addresses proposed. - // - duration_epochs: The duration in epochs before the offer expires. - public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { - // Propose the offer on the signer's account - let addr = signer::address_of(sig); - + fun ensure_valid_propose_offer_state(addr: address) { // Ensure the account is not yet initialized as multisig assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); // Ensure the account has governance initialized and offer structure assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); - assert!(exists_offer(addr), error::not_found(ENOT_OFFERED)); + assert!(exists_offer(addr), error::already_exists(EOFFER_ALREADY_EXISTS)); + } + + fun ensure_valid_propose_offer_params(addr: address, proposed: vector
, duration_epochs: Option) { // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); @@ -301,24 +272,27 @@ module ol_framework::multi_action { // Ensure distinct addresses and multisign owner not in the list multisig_account::validate_owners(&proposed, addr); - // Ensure the proposed list address are valid - let i = 0; - while (i < vector::length(&proposed)) { - let proposed_addr = vector::borrow(&proposed, i); - assert!(account::exists_at(*proposed_addr), error::not_found(EPROPOSED_NOT_EXISTS)); - i = i + 1; + if (option::is_some(&duration_epochs)) { + let duration_epochs = *option::borrow(&duration_epochs); + // Ensure duration is greater than zero + assert!(duration_epochs > 0, error::invalid_argument(EZERO_DURATION)); }; - + } + + // Calculate the expiration epoch for the offer. + fun calculate_expiration_epoch(duration_epochs: Option): u64 { let duration_epochs = if (option::is_some(&duration_epochs)) { *option::borrow(&duration_epochs) } else { DEFAULT_EPOCHS_OFFER_EXPIRE }; - // Ensure duration is greater than zero - assert!(duration_epochs > 0, error::invalid_argument(EZERO_DURATION)); + epoch_helper::get_current_epoch() + duration_epochs + } - let expiration_epoch = epoch_helper::get_current_epoch() + duration_epochs; + // Update the offer with the new proposed authorities and expiration epoch. + fun update_offer(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { + let expiration_epoch = calculate_expiration_epoch(duration_epochs); // Update offer let offer = borrow_global_mut(addr); @@ -356,7 +330,7 @@ module ol_framework::multi_action { }; }; - // Update proposed and expiration epoch lists + // Insert/Update proposed and expiration epoch lists let k = 0; while (k < vector::length(&proposed)) { // if already contains the address, update the expiration_epoch @@ -372,6 +346,19 @@ module ol_framework::multi_action { k = k + 1; }; } + + // Propose an offer to new authorities on the signer account + // or update the expiration epoch of the existing proposed authorities. + // - sig: The signer proposing the offer. + // - proposed: The list of authorities addresses proposed. + // - duration_epochs: The duration in epochs before the offer expires. + public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + // Propose the offer on the signer's account + let addr = signer::address_of(sig); + ensure_valid_propose_offer_state(addr); + ensure_valid_propose_offer_params(addr, proposed, duration_epochs); + update_offer(addr, proposed, duration_epochs); + } // Allows a proposed authority to claim their offer. // - sig: The signer making the claim. @@ -463,22 +450,6 @@ module ol_framework::multi_action { assert!(is_authority(multisig_address, sender_addr), error::invalid_argument(ENOT_AUTHORIZED)); } - // TODO: remove this function after offer migration is completed - #[test_only] - public entry fun finalize_and_cage_deprecated(sig: &signer, initial_authorities: - vector
, num_signers: u64) { - let addr = signer::address_of(sig); - assert!(exists(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - assert!(exists>(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - // not yet initialized - assert!(!multisig_account::is_multisig(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - - multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); - } - // TODO: remove this function after dependencies are updated public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { @@ -1024,4 +995,44 @@ module ol_framework::multi_action { let prop = ballot::get_type_struct(b); prop.expiration_epoch } + + + // TODO: remove this after offer migration is completed + #[test_only] + public(friend) fun init_gov_deprecated(sig: &signer) { + let multisig_address = signer::address_of(sig); + + if (!exists(multisig_address)) { + move_to(sig, Governance { + cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, + cfg_default_n_sigs: 0, // deprecate + signers: vector::empty(), + withdraw_capability: option::none(), + guid_capability: account::create_guid_capability(sig), + }); + }; + + if (!exists>(multisig_address)) { + move_to(sig, Action { + can_withdraw: false, + vote: ballot::new_tracker>(), + }); + }; + } + + // TODO: remove this function after offer migration is completed + #[test_only] + public entry fun finalize_and_cage_deprecated(sig: &signer, initial_authorities: + vector
, num_signers: u64) { + let addr = signer::address_of(sig); + assert!(exists(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + // not yet initialized + assert!(!multisig_account::is_multisig(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + + multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); + } } From 78bd05bcc20a005fd21c1513fbaf05f70117b506 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:19:58 -0300 Subject: [PATCH 25/68] fixes indentation --- .../tests/vote_lib/multi_action.test.move | 2404 ++++++++--------- .../ol_sources/vote_lib/multi_action.move | 1984 +++++++------- 2 files changed, 2194 insertions(+), 2194 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 9a2bec7ce..dc6d594c8 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -1,1206 +1,1206 @@ #[test_only] module ol_framework::test_multi_action { - use ol_framework::mock; - use ol_framework::multi_action; - use ol_framework::safe; - use std::signer; - use std::option; - use std::vector; - use std::guid; - use ol_framework::ol_account; - use diem_framework::resource_account; - use diem_framework::reconfiguration; - use diem_framework::account; - - // print - use std::debug::print; - - struct DummyType has drop, store {} - - #[test(root = @ol_framework, carol = @0x1000c)] - fun init_multi_action(root: &signer, carol: &signer) { - mock::genesis_n_vals(root, 2); - - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 7357001); - - // make the vals the signers on the safe - multi_action::init_gov(&resource_sig); - multi_action::init_type(&resource_sig, true); - } - - // Happy Day: propose offer to authorities - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - fun propose_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - let carol_address = @0x1000c; + use ol_framework::mock; + use ol_framework::multi_action; + use ol_framework::safe; + use std::signer; + use std::option; + use std::vector; + use std::guid; + use ol_framework::ol_account; + use diem_framework::resource_account; + use diem_framework::reconfiguration; + use diem_framework::account; + + // print + use std::debug::print; + + struct DummyType has drop, store {} + + #[test(root = @ol_framework, carol = @0x1000c)] + fun init_multi_action(root: &signer, carol: &signer) { + mock::genesis_n_vals(root, 2); + + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(carol, b"0x1"); + let new_resource_address = signer::address_of(&resource_sig); + assert!(resource_account::is_resource_account(new_resource_address), 7357001); + + // make the vals the signers on the safe + multi_action::init_gov(&resource_sig); + multi_action::init_type(&resource_sig, true); + } + + // Happy Day: propose offer to authorities + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun propose_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let carol_address = @0x1000c; + + // check the offer does not exist + assert!(!multi_action::exists_offer(carol_address), 7357001); + assert!(!multi_action::is_multi_action(carol_address), 7357002); + + // initialize the multi_action account + multi_action::init_gov(carol); + + // offer authorities + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + + // check the offer is proposed and account is not muti_action yet + assert!(multi_action::exists_offer(carol_address), 7357003); + assert!(multi_action::get_offer_proposed(carol_address) == authorities, 7357004); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); + assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 7357006); + let expiration = vector::empty(); + vector::push_back(&mut expiration, 7); + vector::push_back(&mut expiration, 7); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == expiration, 7357007); + assert!(!multi_action::is_multi_action(carol_address), 7357008); + } + + // Propose new offer after expired + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun propose_offer_after_expired(root: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + + // initialize the multi_action account + multi_action::init_gov(carol); + + // offer to alice + multi_action::propose_offer(carol, vector::singleton(@0x1000a), option::some(2)); + + // check the offer is valid + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::singleton(2), 7357004); + + // wait for the offer to expire + mock::trigger_epoch(root); // epoch 1 valid + mock::trigger_epoch(root); // epoch 2 expired + assert!(multi_action::is_offer_expired(carol_address, @0x1000a), 7357005); + + // propose a new offer to bob + multi_action::propose_offer(carol, vector::singleton(@0x1000b), option::some(3)); + + // check the new offer is proposed + assert!(multi_action::get_offer_proposed(carol_address) == vector::singleton(@0x1000b), 7357007); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357008); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::singleton(5), 7357009); + } + + // Happy Day: claim offer by authorities + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun claim_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let carol_address = @0x1000c; + + // initialize the multi_action account + multi_action::init_gov(carol); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + + // bob claim the offer + multi_action::claim_offer(bob, carol_address); + + // check the claimed offer + assert!(multi_action::exists_offer(carol_address), 7357001); + let claimed = vector::singleton(signer::address_of(bob)); + let proposed = vector::singleton(signer::address_of(alice)); + assert!(multi_action::get_offer_claimed(carol_address) == claimed, 7357002); + assert!(multi_action::get_offer_proposed(carol_address) == proposed, 7357003); + + // alice claim the offer + multi_action::claim_offer(alice, carol_address); + + // check alice and bob claimed the offer + let claimed = multi_action::get_offer_claimed(carol_address); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(alice)); + assert!(claimed == authorities, 7357004); + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357005); + } + + // Happy Day: finalize multisign account + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + fun finalize_multi_action(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + + // initialize the multi_action account + multi_action::init_gov(carol); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + + // authorities claim the offer + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + + // finalize the multi_action account + assert!(account::exists_at(carol_address), 7357001); + multi_action::finalize_and_cage2(carol); + + // check the account is multi_action + assert!(multi_action::is_multi_action(carol_address), 7357002); + + // check authorities + let authorities = multi_action::get_authorities(carol_address); + let claimed = vector::empty
(); + vector::push_back(&mut claimed, signer::address_of(alice)); + vector::push_back(&mut claimed, signer::address_of(bob)); + assert!(authorities == claimed, 7357003); + + // check offer was cleaned + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357006); + } + + // Finalize multisign account having a pending claim + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d)] + fun finalize_with_pending_claim(root: &signer, carol: &signer, alice: &signer, bob: &signer, dave: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let carol_address = @0x1000c; + + // initialize the multi_action account + multi_action::init_gov(carol); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(dave)); + multi_action::propose_offer(carol, authorities, option::none()); + + // authorities claim the offer + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + + // finalize the multi_action account + assert!(account::exists_at(carol_address), 7357001); + multi_action::finalize_and_cage2(carol); + + // check the account is multi_action + assert!(multi_action::is_multi_action(carol_address), 7357002); + + // check authorities + let authorities = multi_action::get_authorities(carol_address); + let claimed = vector::empty
(); + vector::push_back(&mut claimed, signer::address_of(alice)); + vector::push_back(&mut claimed, signer::address_of(bob)); + assert!(authorities == claimed, 7357003); + + // check offer was cleared + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357006); + + } + + // Propose another offer with different authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun propose_another_offer_different_authorities(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + multi_action::init_gov(alice); + + // invite bob + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(1)); + + // invite carol and dave + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + multi_action::propose_offer(alice, authorities, option::some(2)); + + // check new authorities + let expiration = vector::singleton(2); + vector::push_back(&mut expiration, 2); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357001); + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357002); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357003); + } + + // Propose new offer with more authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun propose_offer_more_authorities(root: &signer, alice: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + multi_action::init_gov(alice); + + // invite bob + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(1)); + + // new invite bob and carol + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x1000b); + vector::push_back(&mut authorities, @0x1000c); + multi_action::propose_offer(alice, authorities, option::some(2)); + + // check offer + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); + let expiration = vector::singleton(2); + vector::push_back(&mut expiration, 2); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + + // carol claim the offer + multi_action::claim_offer(carol, @0x1000a); + + // new invite bob, carol and dave + vector::push_back(&mut authorities, @0x1000d); + multi_action::propose_offer(alice, authorities, option::some(3)); + + // check new authorities + let proposed = vector::singleton(@0x1000b); + vector::push_back(&mut proposed, @0x1000d); + let expiration = vector::singleton(3); + vector::push_back(&mut expiration, 3); + assert!(multi_action::get_offer_proposed(@0x1000a) == proposed, 7357004); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 7357005); + } + + // Propose new offer with less authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun propose_offer_less_authorities(root: &signer, alice: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + multi_action::init_gov(alice); + + // invite bob, carol e dave + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + multi_action::propose_offer(alice, authorities, option::some(2)); + + // new invite bob e carol + let new_authorities = vector::singleton(@0x1000b); + vector::push_back(&mut new_authorities, @0x1000c); + multi_action::propose_offer(alice, new_authorities, option::some(3)); + + // check new authorities minus dave + assert!(multi_action::get_offer_proposed(@0x1000a) == new_authorities, 7357001); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); + let expiration = vector::singleton(3); + vector::push_back(&mut expiration, 3); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + + // carol claim the offer + multi_action::claim_offer(carol, @0x1000a); + + // new invite bob only + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(4)); + + // check new authorities minus carol + assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000b), 7357003); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357004); + } + + // Propose new offer with same authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun propose_offer_same_authorities(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + multi_action::init_gov(alice); + + // invite bob e carol + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x1000b); + vector::push_back(&mut authorities, @0x1000c); + multi_action::propose_offer(alice, authorities, option::some(2)); + + // check offer + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); + let expiration = vector::singleton(2); + vector::push_back(&mut expiration, 2); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + + // new invite bob e carol + multi_action::propose_offer(alice, authorities, option::some(3)); + + // check offer + assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); + let expiration = vector::singleton(3); + vector::push_back(&mut expiration, 3); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + + // bob claim the offer + multi_action::claim_offer(bob, @0x1000a); + + // new invite bob e carol + multi_action::propose_offer(alice, authorities, option::some(4)); + + // check authorities + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector::singleton(4), 7357002); + assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000c), 7357003); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000b), 7357004); + } + + // Try to propose offer without governance + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] + fun propose_offer_without_gov(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + } + + // Try to propose offer to an multisig account + #[test(root = @ol_framework, dave = @0x1000d, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x30019, location = ol_framework::multi_action)] + fun propose_offer_to_multisign(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let carol_address = @0x1000c; + let dave_address = @0x1000d; + multi_action::init_gov(carol); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + multi_action::finalize_and_cage2(carol); + + // propose offer to multisig account + multi_action::propose_offer(carol, vector::singleton(dave_address), option::none()); + } + + // Try to propose an empty offer + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x10016, location = ol_framework::multi_action)] + fun propose_empty_offer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + multi_action::init_gov(alice); + multi_action::propose_offer(alice, vector::empty
(), option::none()); + } + + // Try to propose too many authorities + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x10024, location = ol_framework::multi_action)] + fun propose_too_many_authorities(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, @0x10001); + vector::push_back(&mut authorities, @0x10002); + vector::push_back(&mut authorities, @0x10003); + vector::push_back(&mut authorities, @0x10004); + vector::push_back(&mut authorities, @0x10005); + vector::push_back(&mut authorities, @0x10006); + vector::push_back(&mut authorities, @0x10007); + vector::push_back(&mut authorities, @0x10008); + vector::push_back(&mut authorities, @0x10009); + vector::push_back(&mut authorities, @0x10010); + vector::push_back(&mut authorities, @0x10011); + multi_action::propose_offer(alice, authorities, option::none()); + } + + // Try to propose offer to the signer address + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x10013, location = ol_framework::multisig_account)] + fun propose_offer_to_signer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + let alice_address = signer::address_of(alice); + multi_action::init_gov(alice); + multi_action::propose_offer(alice, vector::singleton
(alice_address), option::none()); + } + + // Try to propose offer with duplicated addresses + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x10001, location = ol_framework::multisig_account)] + fun propose_offer_duplicated_authorities(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + multi_action::init_gov(alice); + + let authorities = vector::singleton
(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000b); + multi_action::propose_offer(alice, authorities, option::none()); + } + + // Try to propose offer to an invalid signer + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x60018, location = ol_framework::multisig_account)] + fun offer_to_invalid_authority(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + multi_action::init_gov(alice); + + // propose to invalid address + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, @0xCAFE); + multi_action::propose_offer(alice, authorities, option::some(2)); + } + + // Try to propose offer with zero duration epochs + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x10022, location = ol_framework::multi_action)] + fun offer_with_zero_duration(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + multi_action::init_gov(alice); + + // propose to invalid address + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(alice, authorities, option::some(0)); + } + + // Try to claim offer not offered to signer + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x60020, location = ol_framework::multi_action)] + fun claim_offer_not_offered(root: &signer, alice: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + multi_action::init_gov(carol); + + // invite bob + multi_action::propose_offer(carol, vector::singleton(@0x1000b), option::none()); + + // alice try to claim the offer + multi_action::claim_offer(alice, carol_address); + } + + // Try to claim expired offer + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x20015, location = ol_framework::multi_action)] + fun claim_expired_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + multi_action::init_gov(carol); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::some(2)); + + // alice claim the offer + multi_action::claim_offer(alice, carol_address); + + mock::trigger_epoch(root); // epoch 1 valid + mock::trigger_epoch(root); // epoch 2 valid + mock::trigger_epoch(root); // epoch 3 expired + + // bob claim expired offer + multi_action::claim_offer(bob, carol_address); + } + + // Try to claim offer of an account without proposal + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x60017, location = ol_framework::multi_action)] + fun claim_offer_without_proposal(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let bob_address = @0x1000c; + multi_action::claim_offer(alice, bob_address); + } + + // Try to claim offer twice + #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x80023, location = ol_framework::multi_action)] + fun claim_offer_twice(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let carol_address = @0x1000c; + multi_action::init_gov(carol); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::some(2)); + + // Alice claim the offer twice + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(alice, carol_address); + } + + // Try to finalize account without governance + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] + fun finalize_without_gov(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + multi_action::finalize_and_cage2(alice); + } + + // Try to finalize account without offer + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x30018, location = ol_framework::multi_action)] + fun finalize_without_offer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + multi_action::init_gov(alice); + multi_action::finalize_and_cage2(alice); + } + + // Try to finalize account without enough offer claimed + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x30018, location = ol_framework::multi_action)] + fun finalize_without_enough_claimed(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + multi_action::init_gov(alice); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + + // bob claim the offer + multi_action::claim_offer(bob, alice_address); + + // finalize the multi_action account + multi_action::finalize_and_cage2(alice); + } + + // Try to finalize account already finalized + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x80019, location = ol_framework::multi_action)] + fun finalize_already_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + multi_action::init_gov(alice); + + // invite the vals to the resource account + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + + // bob claim the offer + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + + // finalize the multi_action account + multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage2(alice); + } + + // Offer Migration tests + + // Happy Day: init offer on a legacy multisign initiated account + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + fun migrate_offer_account_initialized(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov_deprecated(alice); + assert!(multi_action::exists_offer(alice_address) == false, 7357001); + + // migrate the offer + multi_action::migrate_offer(alice, alice_address); + assert!(multi_action::exists_offer(alice_address) == true, 7357002); + + // offer authority to bob + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(9)); + + // check the offer is proposed + assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000b), 7357003); + assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357004); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(9), 7357005); + } + + // Happy Day: init offer on a legacy mulstisign finalized account + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun migrate_offer_account_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let alice_address = signer::address_of(alice); + + // make alice account multisign with deprecated methods + multi_action::init_gov_deprecated(alice); + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + multi_action::finalize_and_cage_deprecated(alice, authorities, 2); + assert!(multi_action::exists_offer(alice_address) == false, 7357001); + + // carol migrate the offer + multi_action::migrate_offer(carol, alice_address); + assert!(multi_action::exists_offer(alice_address) == true, 7357002); + + // carol propose dave as new authority + let id = multi_action::propose_governance(carol, alice_address, + vector::singleton(@0x1000d), true, option::none(), + option::none()); + + // bob votes + let passed = multi_action::vote_governance(bob, alice_address, &id); + assert!(passed, 7357003); + + // dave claim the offer + multi_action::claim_offer(dave, alice_address); + + // check new authorities + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + assert!(multi_action::get_authorities(alice_address) == authorities, 7357004); + } + + // Try to migrate offer of an account already with offer + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x80025, location = ol_framework::multi_action)] + fun migrate_offer_account_with_offer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov(alice); + + // try to migrate offer + multi_action::migrate_offer(alice, alice_address); + } + + // Try to migrate offer of an account without governance initiated + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] + fun migrate_offer_account_without_gov(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + let alice_address = signer::address_of(alice); + + // try to migrate offer + multi_action::migrate_offer(alice, alice_address); + } + + // Try to migrate offer through someone without authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x50003, location = ol_framework::multi_action)] + fun migrate_offer_without_authority(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov_deprecated(alice); + + // try to migrate offer + multi_action::migrate_offer(bob, alice_address); + } + + // Governance Tests - // check the offer does not exist - assert!(!multi_action::exists_offer(carol_address), 7357001); - assert!(!multi_action::is_multi_action(carol_address), 7357002); - - // initialize the multi_action account - multi_action::init_gov(carol); - - // offer authorities - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); - - // check the offer is proposed and account is not muti_action yet - assert!(multi_action::exists_offer(carol_address), 7357003); - assert!(multi_action::get_offer_proposed(carol_address) == authorities, 7357004); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); - assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 7357006); - let expiration = vector::empty(); - vector::push_back(&mut expiration, 7); - vector::push_back(&mut expiration, 7); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == expiration, 7357007); - assert!(!multi_action::is_multi_action(carol_address), 7357008); - } - - // Propose new offer after expired - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - fun propose_offer_after_expired(root: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let carol_address = @0x1000c; - - // initialize the multi_action account - multi_action::init_gov(carol); - - // offer to alice - multi_action::propose_offer(carol, vector::singleton(@0x1000a), option::some(2)); - - // check the offer is valid - assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::singleton(2), 7357004); - - // wait for the offer to expire - mock::trigger_epoch(root); // epoch 1 valid - mock::trigger_epoch(root); // epoch 2 expired - assert!(multi_action::is_offer_expired(carol_address, @0x1000a), 7357005); - - // propose a new offer to bob - multi_action::propose_offer(carol, vector::singleton(@0x1000b), option::some(3)); - - // check the new offer is proposed - assert!(multi_action::get_offer_proposed(carol_address) == vector::singleton(@0x1000b), 7357007); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357008); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::singleton(5), 7357009); - } - - // Happy Day: claim offer by authorities - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - fun claim_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let carol_address = @0x1000c; - - // initialize the multi_action account - multi_action::init_gov(carol); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); - - // bob claim the offer - multi_action::claim_offer(bob, carol_address); - - // check the claimed offer - assert!(multi_action::exists_offer(carol_address), 7357001); - let claimed = vector::singleton(signer::address_of(bob)); - let proposed = vector::singleton(signer::address_of(alice)); - assert!(multi_action::get_offer_claimed(carol_address) == claimed, 7357002); - assert!(multi_action::get_offer_proposed(carol_address) == proposed, 7357003); - - // alice claim the offer - multi_action::claim_offer(alice, carol_address); - - // check alice and bob claimed the offer - let claimed = multi_action::get_offer_claimed(carol_address); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(alice)); - assert!(claimed == authorities, 7357004); - assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357005); - } - - // Happy Day: finalize multisign account - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - fun finalize_multi_action(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let carol_address = @0x1000c; - - // initialize the multi_action account - multi_action::init_gov(carol); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); - - // authorities claim the offer - multi_action::claim_offer(alice, carol_address); - multi_action::claim_offer(bob, carol_address); - - // finalize the multi_action account - assert!(account::exists_at(carol_address), 7357001); - multi_action::finalize_and_cage2(carol); - - // check the account is multi_action - assert!(multi_action::is_multi_action(carol_address), 7357002); - - // check authorities - let authorities = multi_action::get_authorities(carol_address); - let claimed = vector::empty
(); - vector::push_back(&mut claimed, signer::address_of(alice)); - vector::push_back(&mut claimed, signer::address_of(bob)); - assert!(authorities == claimed, 7357003); - - // check offer was cleaned - assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357006); - } - - // Finalize multisign account having a pending claim - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d)] - fun finalize_with_pending_claim(root: &signer, carol: &signer, alice: &signer, bob: &signer, dave: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - let carol_address = @0x1000c; - - // initialize the multi_action account - multi_action::init_gov(carol); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(dave)); - multi_action::propose_offer(carol, authorities, option::none()); - - // authorities claim the offer - multi_action::claim_offer(alice, carol_address); - multi_action::claim_offer(bob, carol_address); - - // finalize the multi_action account - assert!(account::exists_at(carol_address), 7357001); - multi_action::finalize_and_cage2(carol); - - // check the account is multi_action - assert!(multi_action::is_multi_action(carol_address), 7357002); - - // check authorities - let authorities = multi_action::get_authorities(carol_address); - let claimed = vector::empty
(); - vector::push_back(&mut claimed, signer::address_of(alice)); - vector::push_back(&mut claimed, signer::address_of(bob)); - assert!(authorities == claimed, 7357003); - - // check offer was cleared - assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357006); - - } - - // Propose another offer with different authorities - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - fun propose_another_offer_different_authorities(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - multi_action::init_gov(alice); - - // invite bob - multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(1)); - - // invite carol and dave - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x1000c); - vector::push_back(&mut authorities, @0x1000d); - multi_action::propose_offer(alice, authorities, option::some(2)); - - // check new authorities - let expiration = vector::singleton(2); - vector::push_back(&mut expiration, 2); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357001); - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357002); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357003); - } - - // Propose new offer with more authorities - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - fun propose_offer_more_authorities(root: &signer, alice: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - multi_action::init_gov(alice); - - // invite bob - multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(1)); - - // new invite bob and carol - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x1000b); - vector::push_back(&mut authorities, @0x1000c); - multi_action::propose_offer(alice, authorities, option::some(2)); - - // check offer - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); - let expiration = vector::singleton(2); - vector::push_back(&mut expiration, 2); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); - - // carol claim the offer - multi_action::claim_offer(carol, @0x1000a); - - // new invite bob, carol and dave - vector::push_back(&mut authorities, @0x1000d); - multi_action::propose_offer(alice, authorities, option::some(3)); - - // check new authorities - let proposed = vector::singleton(@0x1000b); - vector::push_back(&mut proposed, @0x1000d); - let expiration = vector::singleton(3); - vector::push_back(&mut expiration, 3); - assert!(multi_action::get_offer_proposed(@0x1000a) == proposed, 7357004); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 7357005); - } - - // Propose new offer with less authorities - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - fun propose_offer_less_authorities(root: &signer, alice: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - multi_action::init_gov(alice); - - // invite bob, carol e dave - let authorities = vector::singleton(@0x1000b); - vector::push_back(&mut authorities, @0x1000c); - vector::push_back(&mut authorities, @0x1000d); - multi_action::propose_offer(alice, authorities, option::some(2)); - - // new invite bob e carol - let new_authorities = vector::singleton(@0x1000b); - vector::push_back(&mut new_authorities, @0x1000c); - multi_action::propose_offer(alice, new_authorities, option::some(3)); - - // check new authorities minus dave - assert!(multi_action::get_offer_proposed(@0x1000a) == new_authorities, 7357001); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); - let expiration = vector::singleton(3); - vector::push_back(&mut expiration, 3); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); - - // carol claim the offer - multi_action::claim_offer(carol, @0x1000a); - - // new invite bob only - multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(4)); - - // check new authorities minus carol - assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000b), 7357003); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357004); - } - - // Propose new offer with same authorities - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - fun propose_offer_same_authorities(root: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - multi_action::init_gov(alice); - - // invite bob e carol - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x1000b); - vector::push_back(&mut authorities, @0x1000c); - multi_action::propose_offer(alice, authorities, option::some(2)); - - // check offer - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); - let expiration = vector::singleton(2); - vector::push_back(&mut expiration, 2); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); - - // new invite bob e carol - multi_action::propose_offer(alice, authorities, option::some(3)); - - // check offer - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); - let expiration = vector::singleton(3); - vector::push_back(&mut expiration, 3); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); - - // bob claim the offer - multi_action::claim_offer(bob, @0x1000a); - - // new invite bob e carol - multi_action::propose_offer(alice, authorities, option::some(4)); - - // check authorities - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector::singleton(4), 7357002); - assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000c), 7357003); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000b), 7357004); - } - - // Try to propose offer without governance - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] - fun propose_offer_without_gov(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); - } - - // Try to propose offer to an multisig account - #[test(root = @ol_framework, dave = @0x1000d, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x30019, location = ol_framework::multi_action)] - fun propose_offer_to_multisign(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - let carol_address = @0x1000c; - let dave_address = @0x1000d; - multi_action::init_gov(carol); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); - multi_action::claim_offer(alice, carol_address); - multi_action::claim_offer(bob, carol_address); - multi_action::finalize_and_cage2(carol); - - // propose offer to multisig account - multi_action::propose_offer(carol, vector::singleton(dave_address), option::none()); - } - - // Try to propose an empty offer - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x10016, location = ol_framework::multi_action)] - fun propose_empty_offer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - multi_action::init_gov(alice); - multi_action::propose_offer(alice, vector::empty
(), option::none()); - } - - // Try to propose too many authorities - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x10024, location = ol_framework::multi_action)] - fun propose_too_many_authorities(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); - multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x10001); - vector::push_back(&mut authorities, @0x10002); - vector::push_back(&mut authorities, @0x10003); - vector::push_back(&mut authorities, @0x10004); - vector::push_back(&mut authorities, @0x10005); - vector::push_back(&mut authorities, @0x10006); - vector::push_back(&mut authorities, @0x10007); - vector::push_back(&mut authorities, @0x10008); - vector::push_back(&mut authorities, @0x10009); - vector::push_back(&mut authorities, @0x10010); - vector::push_back(&mut authorities, @0x10011); - multi_action::propose_offer(alice, authorities, option::none()); - } - - // Try to propose offer to the signer address - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x10013, location = ol_framework::multisig_account)] - fun propose_offer_to_signer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); - let alice_address = signer::address_of(alice); - multi_action::init_gov(alice); - multi_action::propose_offer(alice, vector::singleton
(alice_address), option::none()); - } - - // Try to propose offer with duplicated addresses - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x10001, location = ol_framework::multisig_account)] - fun propose_offer_duplicated_authorities(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - multi_action::init_gov(alice); - - let authorities = vector::singleton
(@0x1000b); - vector::push_back(&mut authorities, @0x1000c); - vector::push_back(&mut authorities, @0x1000b); - multi_action::propose_offer(alice, authorities, option::none()); - } - - // Try to propose offer to an invalid signer - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x60018, location = ol_framework::multisig_account)] - fun offer_to_invalid_authority(root: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - multi_action::init_gov(alice); - - // propose to invalid address - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, @0xCAFE); - multi_action::propose_offer(alice, authorities, option::some(2)); - } - - // Try to propose offer with zero duration epochs - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x10022, location = ol_framework::multi_action)] - fun offer_with_zero_duration(root: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - multi_action::init_gov(alice); - - // propose to invalid address - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(alice, authorities, option::some(0)); - } - - // Try to claim offer not offered to signer - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x60020, location = ol_framework::multi_action)] - fun claim_offer_not_offered(root: &signer, alice: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let carol_address = @0x1000c; - multi_action::init_gov(carol); - - // invite bob - multi_action::propose_offer(carol, vector::singleton(@0x1000b), option::none()); - - // alice try to claim the offer - multi_action::claim_offer(alice, carol_address); - } - - // Try to claim expired offer - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x20015, location = ol_framework::multi_action)] - fun claim_expired_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let carol_address = @0x1000c; - multi_action::init_gov(carol); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::some(2)); - - // alice claim the offer - multi_action::claim_offer(alice, carol_address); - - mock::trigger_epoch(root); // epoch 1 valid - mock::trigger_epoch(root); // epoch 2 valid - mock::trigger_epoch(root); // epoch 3 expired - - // bob claim expired offer - multi_action::claim_offer(bob, carol_address); - } - - // Try to claim offer of an account without proposal - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x60017, location = ol_framework::multi_action)] - fun claim_offer_without_proposal(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let bob_address = @0x1000c; - multi_action::claim_offer(alice, bob_address); - } - - // Try to claim offer twice - #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x80023, location = ol_framework::multi_action)] - fun claim_offer_twice(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let carol_address = @0x1000c; - multi_action::init_gov(carol); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::some(2)); - - // Alice claim the offer twice - multi_action::claim_offer(alice, carol_address); - multi_action::claim_offer(alice, carol_address); - } - - // Try to finalize account without governance - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] - fun finalize_without_gov(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); - multi_action::finalize_and_cage2(alice); - } - - // Try to finalize account without offer - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x30018, location = ol_framework::multi_action)] - fun finalize_without_offer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); - multi_action::init_gov(alice); - multi_action::finalize_and_cage2(alice); - } - - // Try to finalize account without enough offer claimed - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x30018, location = ol_framework::multi_action)] - fun finalize_without_enough_claimed(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; - multi_action::init_gov(alice); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); - - // bob claim the offer - multi_action::claim_offer(bob, alice_address); - - // finalize the multi_action account - multi_action::finalize_and_cage2(alice); - } - - // Try to finalize account already finalized - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x80019, location = ol_framework::multi_action)] - fun finalize_already_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; - multi_action::init_gov(alice); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); - - // bob claim the offer - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - - // finalize the multi_action account - multi_action::finalize_and_cage2(alice); - multi_action::finalize_and_cage2(alice); - } - - // Offer Migration tests - - // Happy Day: init offer on a legacy multisign initiated account - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - fun migrate_offer_account_initialized(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let alice_address = signer::address_of(alice); - - // initialize the multi_action account - multi_action::init_gov_deprecated(alice); - assert!(multi_action::exists_offer(alice_address) == false, 7357001); - - // migrate the offer - multi_action::migrate_offer(alice, alice_address); - assert!(multi_action::exists_offer(alice_address) == true, 7357002); - - // offer authority to bob - multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(9)); - - // check the offer is proposed - assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000b), 7357003); - assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357004); - assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(9), 7357005); - } - - // Happy Day: init offer on a legacy mulstisign finalized account - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - fun migrate_offer_account_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - let alice_address = signer::address_of(alice); - - // make alice account multisign with deprecated methods - multi_action::init_gov_deprecated(alice); - let authorities = vector::singleton(@0x1000b); - vector::push_back(&mut authorities, @0x1000c); - multi_action::finalize_and_cage_deprecated(alice, authorities, 2); - assert!(multi_action::exists_offer(alice_address) == false, 7357001); - - // carol migrate the offer - multi_action::migrate_offer(carol, alice_address); - assert!(multi_action::exists_offer(alice_address) == true, 7357002); - - // carol propose dave as new authority - let id = multi_action::propose_governance(carol, alice_address, - vector::singleton(@0x1000d), true, option::none(), - option::none()); - - // bob votes - let passed = multi_action::vote_governance(bob, alice_address, &id); - assert!(passed, 7357003); - - // dave claim the offer - multi_action::claim_offer(dave, alice_address); - - // check new authorities - let authorities = vector::singleton(@0x1000b); - vector::push_back(&mut authorities, @0x1000c); - vector::push_back(&mut authorities, @0x1000d); - assert!(multi_action::get_authorities(alice_address) == authorities, 7357004); - } - - // Try to migrate offer of an account already with offer - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x80025, location = ol_framework::multi_action)] - fun migrate_offer_account_with_offer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let alice_address = signer::address_of(alice); - - // initialize the multi_action account - multi_action::init_gov(alice); - - // try to migrate offer - multi_action::migrate_offer(alice, alice_address); - } - - // Try to migrate offer of an account without governance initiated - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] - fun migrate_offer_account_without_gov(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); - let alice_address = signer::address_of(alice); - - // try to migrate offer - multi_action::migrate_offer(alice, alice_address); - } - - // Try to migrate offer through someone without authority - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x50003, location = ol_framework::multi_action)] - fun migrate_offer_without_authority(root: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let alice_address = signer::address_of(alice); - - // initialize the multi_action account - multi_action::init_gov_deprecated(alice); - - // try to migrate offer - multi_action::migrate_offer(bob, alice_address); - } - - // Governance Tests - - // Happy Day: propose a new action and check zero votes - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - fun propose_action(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; - - // offer to bob and carol authority on the alice safe - multi_action::init_gov(alice); - multi_action::init_type(alice, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); - - // bob and alice claim the offer - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - - // alice finalize multi action workflow to release control of the account - multi_action::finalize_and_cage2(alice); - - // bob create a proposal - let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - let id = multi_action::propose_new(bob, alice_address, proposal); - - // SHOULD NOT HAVE COUNTED ANY VOTES - let v = multi_action::get_votes(alice_address, guid::id_creation_num(&id)); - assert!(vector::length(&v) == 0, 7357001); - } - - // Multisign authorities bob and carol try to send the same proposal - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - fun propose_action_prevent_duplicated(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; - - // offer to bob and carol authority on the alice safe - multi_action::init_gov(alice); - multi_action::init_type(alice, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); - - // bob and alice claim the offer - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - - // alice finalize multi action workflow to release control of the account - multi_action::finalize_and_cage2(alice); - - let count = multi_action::get_count_of_pending(alice_address); - assert!(count == 0, 7357001); - - let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - let id = multi_action::propose_new(bob, alice_address, proposal); - let count = multi_action::get_count_of_pending(alice_address); - assert!(count == 1, 7357002); - - let epoch_ending = multi_action::get_expiration(alice_address, guid::id_creation_num(&id)); - assert!(epoch_ending == 14, 7357003); - - let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - multi_action::propose_new(carol, alice_address, proposal); - let count = multi_action::get_count_of_pending(alice_address); - assert!(count == 1, 7357004); // no change - - // confirm there are no votes - let v = multi_action::get_votes(alice_address, guid::id_creation_num(&id)); - assert!(vector::length(&v) == 0, 7357005); - - // proposing a different ending epoch will have no effect on the proposal once it is started. Here we try to set to epoch 4, but nothing should change, at it will still be 14. - let proposal = multi_action::proposal_constructor(DummyType{}, option::some(4)); - multi_action::propose_new(carol, alice_address, proposal); - let count = multi_action::get_count_of_pending(alice_address); - assert!(count == 1, 7357006); - let epoch = multi_action::get_expiration(alice_address, guid::id_creation_num(&id)); - assert!(epoch == 14, 7357007); - } - - // Happy day: complete vote action - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - fun vote_action_happy_simple(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - // Scenario: a simple MultiAction where we don't need any capabilities. Only need to know if the result was successful on the vote that crossed the threshold. - - // transform alice account in multisign with bob and carol as authorities - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; - multi_action::init_gov(alice); - // Ths is a simple multi_action: there is no capability being stored - multi_action::init_type(alice, false); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); - - // bob create a proposal - let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - let id = multi_action::propose_new(bob, alice_address, proposal); - let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, alice_address); - assert!(passed == false, 7357001); - option::destroy_none(cap_opt); - - let (passed, cap_opt) = multi_action::vote_with_id(carol, &id, alice_address); - assert!(passed == true, 7357002); - // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED - assert!(option::is_none(&cap_opt), 7357003); - - option::destroy_none(cap_opt); - } - - // Happy day: complete vote action with withdraw capability - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - fun vote_action_happy_withdraw_cap(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - // Scenario: testing that a payment type multisig could be created with this module: that the WithdrawCapability can be used here. - - let _vals = mock::genesis_n_vals(root, 4); - mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - let alice_address = @0x1000a; - - // fund the alice multi_action's account - ol_account::transfer(alice, alice_address, 100); - - // make the bob and carol the signers on the alice safe, and 2-of-2 need to sign - multi_action::init_gov(alice); - multi_action::init_type(alice, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); - - // bob create a proposal and vote - let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - let id = multi_action::propose_new(bob, alice_address, proposal); - let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, alice_address); - assert!(passed == false, 7357001); - option::destroy_none(cap_opt); - - // carol vote on bob proposal - let (passed, cap_opt) = multi_action::vote_with_id(carol, &id, alice_address); - assert!(passed == true, 7357002); - - // THE WITHDRAW CAPABILITY IS WHERE WE EXPECT - assert!(option::is_some(&cap_opt), 7357003); - let cap = option::extract(&mut cap_opt); - let c = ol_account::withdraw_with_capability( - &cap, - 42, - ); - // deposit to erik account - ol_account::create_account(root, @0x1000e); - ol_account::deposit_coins(@0x1000e, c); - option::fill(&mut cap_opt, cap); - // check erik account balance - let (_, balance) = ol_account::balance(@0x1000e); - assert!(balance == 42, 7357004); - - multi_action::maybe_restore_withdraw_cap(cap_opt); - } - - // Try to vote on a closed ballot - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - #[expected_failure(abort_code = 0x30012, location = ol_framework::multi_action)] - fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, dave: &signer) { - // Scenario: Testing that if an action expires voting cannot be done. - - let _vals = mock::genesis_n_vals(root, 3); - mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - - // we are at epoch 0 - let epoch = reconfiguration::get_current_epoch(); - assert!(epoch == 0, 7357001); - - // dave creates erik resource account. He is not one of the validators, and is not an authority in the multisig. - let (erik, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1000e"); - let erik_address = signer::address_of(&erik); - assert!(resource_account::is_resource_account(erik_address), 7357002); - - // fund the account - ol_account::transfer(alice, erik_address, 100); - // offer alice and bob authority on the safe - safe::init_payment_multisig(&erik); // both need to sign - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(&erik, authorities, option::none()); - multi_action::claim_offer(alice, erik_address); - multi_action::claim_offer(bob, erik_address); - multi_action::finalize_and_cage2(&erik); - - // make a proposal for governance, expires in 2 epoch from now - let id = multi_action::propose_governance(alice, erik_address, vector::empty(), true, option::some(1), option::some(2)); - - mock::trigger_epoch(root); // epoch 1 - mock::trigger_epoch(root); // epoch 2 - mock::trigger_epoch(root); // epoch 3 -- voting ended here - mock::trigger_epoch(root); // epoch 4 -- now expired - - let epoch = reconfiguration::get_current_epoch(); - assert!(epoch == 4, 7357003); - - // trying to vote on a closed ballot will error - let _passed = multi_action::vote_governance(bob, erik_address, &id); - } - - - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - fun governance_change_auths(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { - // Scenario: The multisig gets initiated with the 2 validators as the only authorities. IT takes 2-of-2 to sign. - // later they add a third (Dave) so it becomes a 2-of-3. - // Dave and Bob, then remove alice so it becomes 2-of-2 again - - let _vals = mock::genesis_n_vals(root, 4); - mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - let carol_address = @0x1000c; - let dave_address = @0x1000d; - - // fund the account - ol_account::transfer(alice, carol_address, 100); - // offer alice and bob authority on the safe - multi_action::init_gov(carol);// both need to sign - multi_action::init_type(carol, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); - multi_action::claim_offer(alice, carol_address); - multi_action::claim_offer(bob, carol_address); - multi_action::finalize_and_cage2(carol); - - // alice is going to propose to change the authorities to add dave - let id = multi_action::propose_governance(alice, carol_address, - vector::singleton(dave_address), true, option::none(), - option::none()); - - // check authorities did not change - let ret = multi_action::get_authorities(carol_address); - assert!(ret == authorities, 7357001); - - // bob votes. bob could either use vote_governance() - let passed = multi_action::vote_governance(bob, carol_address, &id); - assert!(passed, 7357002); - - // check authorities did not change - let ret = multi_action::get_authorities(carol_address); - assert!(ret == authorities, 7357003); - - // check the Offer - let ret = multi_action::get_offer_proposed(carol_address); - assert!(ret == vector::singleton(dave_address), 7357004); - - // dave claims the offer and it becomes final. - multi_action::claim_offer(dave, carol_address); - - // Chek new set of authorities - let ret = multi_action::get_authorities(carol_address); - vector::push_back(&mut authorities, dave_address); - assert!(ret == authorities, 7357005); - - // Check if offer was cleaned - assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357006); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357007); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357008); - - // Now dave and bob, will conspire to remove alice. - // NOTE: `false` means `remove account` here - let id = multi_action::propose_governance(dave, carol_address, vector::singleton(signer::address_of(alice)), false, option::none(), option::none()); - let a = multi_action::get_authorities(carol_address); - assert!(vector::length(&a) == 3, 7357009); // no change yet - - // bob votes and it becomes final. Bob could either use vote_governance() - let passed = multi_action::vote_governance(bob, carol_address, &id); - assert!(passed, 7357008); - let a = multi_action::get_authorities(carol_address); - assert!(vector::length(&a) == 2, 73570010); - assert!(!multi_action::is_authority(carol_address, signer::address_of(alice)), 7357011); - - // Check if offer was cleaned - assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 73570012); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 73570013); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 73570014); - } - - // Happy day: change the threshold of a multisig - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - fun governance_change_threshold(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { - // Scenario: The multisig gets initiated with the 2 bob and carol as the only authorities. It takes 2-of-2 to sign. - // They decide next only 1-of-2 will be needed. - - let _vals = mock::genesis_n_vals(root, 3); - mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - - // Dave creates the resource account. He is not one of the validators, and is not an authority in the multisig. - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); - let new_resource_address = signer::address_of(&resource_sig); - assert!(resource_account::is_resource_account(new_resource_address), 7357001); - - // fund the account - ol_account::transfer(alice, new_resource_address, 100); - - // offer bob and carol authority on the safe - multi_action::init_gov(&resource_sig);// both need to sign - multi_action::init_type(&resource_sig, false); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(&resource_sig, authorities, option::none()); - multi_action::claim_offer(carol, new_resource_address); - multi_action::claim_offer(bob, new_resource_address); - multi_action::finalize_and_cage2(&resource_sig); - - // carol is going to propose to change the authorities to add Rando - let id = multi_action::propose_governance(carol, new_resource_address, vector::empty(), true, option::some(1), option::none()); - - // check authorities and threshold - let a = multi_action::get_authorities(new_resource_address); - assert!(vector::length(&a) == 2, 7357002); // no change - let (n, _m) = multi_action::get_threshold(new_resource_address); - assert!(n == 2, 7357003); - - // bob votes and it becomes final. Bob could either use vote_governance() - let passed = multi_action::vote_governance(bob, new_resource_address, &id); - - // check authorities and threshold - assert!(passed, 7357004); - let a = multi_action::get_authorities(new_resource_address); - assert!(vector::length(&a) == 2, 7357005); // no change - let (n, _m) = multi_action::get_threshold(new_resource_address); - assert!(n == 1, 7357006); - - // now any other type of action can be taken with just one signer - let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); - let id = multi_action::propose_new(bob, new_resource_address, proposal); - let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, new_resource_address); - assert!(passed == true, 7357007); - - // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED - assert!(option::is_none(&cap_opt), 7357008); - - option::destroy_none(cap_opt); - } - - // Vote new athority before the previous one is claimed - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, erik = @0x1000e)] - fun governance_vote_before_claim(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 5); - let alice_address = @0x1000a; - - // alice offer bob and carol authority on her account - multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); - - // carol is going to propose to change the authorities to add dave - let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), true, option::none(), option::none()); - - // bob votes and dave does not claims the offer - multi_action::vote_governance(bob, alice_address, &id); - - // check authorities and threshold - assert!(multi_action::get_authorities(alice_address) == authorities, 7357001); - let (n, _m) = multi_action::get_threshold(alice_address); - assert!(n == 2, 7357002); - - // check offer - print(&multi_action::get_offer_proposed(alice_address)); - assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357003); - print(&multi_action::get_offer_proposed(alice_address)); - assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000d), 7357004); - assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(7), 7357005); - - mock::trigger_epoch(root); // epoch 1 - - // bob is going to propose to change the authorities to add erik - let id = multi_action::propose_governance(bob, alice_address, vector::singleton(@0x1000e), true, option::none(), option::none()); - - // carol votes - multi_action::vote_governance(carol, alice_address, &id); - - // check authorities and threshold - assert!(multi_action::get_authorities(alice_address) == authorities, 7357001); - let (n, _m) = multi_action::get_threshold(alice_address); - assert!(n == 2, 7357002); - - // check offer - assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357003); - let proposed = vector::singleton(@0x1000d); - vector::push_back(&mut proposed, @0x1000e); - assert!(multi_action::get_offer_proposed(alice_address) == proposed, 7357004); - let expiration = vector::singleton(7); - vector::push_back(&mut expiration, 8); - assert!(multi_action::get_offer_expiration_epoch(alice_address) == expiration, 7357005); - } - - // Try to vote an invalid address for new authority - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x60018, location = ol_framework::multisig_account)] - fun governance_vote_invalid_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; - - // alice offer bob and carol authority on her account - multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); - - // carol is going to propose to change the authorities to add dave - let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0xCAFE), true, option::none(), option::none()); - - // bob votes and dave does not claims the offer - multi_action::vote_governance(bob, alice_address, &id); - } - - // Try to vote duplicated addresses for new authority - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - #[expected_failure(abort_code = 0x10001, location = ol_framework::multisig_account)] - fun governance_vote_duplicated_addresses(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 5); - let alice_address = @0x1000a; - - // alice offer bob and carol authority on her account - multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); - - // carol is going to propose to change the authorities to add dave twice - let authorities = vector::singleton(@0x1000d); - vector::push_back(&mut authorities, @0x1000d); - let _id = multi_action::propose_governance(carol, alice_address, authorities, true, option::none(), option::none()); - } - - // Try to vote multisig account address for new authority - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - #[expected_failure(abort_code = 0x10013, location = ol_framework::multisig_account)] - fun governance_vote_multisig_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 5); - let alice_address = @0x1000a; - - // alice offer bob and carol authority on her account - multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); - - // carol is going to propose to change the authorities to add dave twice - let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(alice_address), true, option::none(), option::none()); - } - - // Try to vote an owner as new authority - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x10026, location = ol_framework::multi_action)] - fun governance_vote_owner_as_new_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; - - // alice offer bob and carol authority on her account - multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); - - // carol is going to propose to change the authorities to add bob - let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(signer::address_of(bob)), true, option::none(), option::none()); - } - - // Try to vote remove an authority that is not in the multisig - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x60027, location = ol_framework::multi_action)] - fun governance_vote_remove_non_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; - - // alice offer bob and carol authority on her account - multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); - multi_action::claim_offer(bob, alice_address); - multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); - - // carol is going to propose to remove dave - let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), false, option::none(), option::none()); - } + // Happy Day: propose a new action and check zero votes + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun propose_action(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + + // offer to bob and carol authority on the alice safe + multi_action::init_gov(alice); + multi_action::init_type(alice, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + + // bob and alice claim the offer + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + + // alice finalize multi action workflow to release control of the account + multi_action::finalize_and_cage2(alice); + + // bob create a proposal + let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); + let id = multi_action::propose_new(bob, alice_address, proposal); + + // SHOULD NOT HAVE COUNTED ANY VOTES + let v = multi_action::get_votes(alice_address, guid::id_creation_num(&id)); + assert!(vector::length(&v) == 0, 7357001); + } + + // Multisign authorities bob and carol try to send the same proposal + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun propose_action_prevent_duplicated(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + + // offer to bob and carol authority on the alice safe + multi_action::init_gov(alice); + multi_action::init_type(alice, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + + // bob and alice claim the offer + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + + // alice finalize multi action workflow to release control of the account + multi_action::finalize_and_cage2(alice); + + let count = multi_action::get_count_of_pending(alice_address); + assert!(count == 0, 7357001); + + let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); + let id = multi_action::propose_new(bob, alice_address, proposal); + let count = multi_action::get_count_of_pending(alice_address); + assert!(count == 1, 7357002); + + let epoch_ending = multi_action::get_expiration(alice_address, guid::id_creation_num(&id)); + assert!(epoch_ending == 14, 7357003); + + let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); + multi_action::propose_new(carol, alice_address, proposal); + let count = multi_action::get_count_of_pending(alice_address); + assert!(count == 1, 7357004); // no change + + // confirm there are no votes + let v = multi_action::get_votes(alice_address, guid::id_creation_num(&id)); + assert!(vector::length(&v) == 0, 7357005); + + // proposing a different ending epoch will have no effect on the proposal once it is started. Here we try to set to epoch 4, but nothing should change, at it will still be 14. + let proposal = multi_action::proposal_constructor(DummyType{}, option::some(4)); + multi_action::propose_new(carol, alice_address, proposal); + let count = multi_action::get_count_of_pending(alice_address); + assert!(count == 1, 7357006); + let epoch = multi_action::get_expiration(alice_address, guid::id_creation_num(&id)); + assert!(epoch == 14, 7357007); + } + + // Happy day: complete vote action + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun vote_action_happy_simple(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + // Scenario: a simple MultiAction where we don't need any capabilities. Only need to know if the result was successful on the vote that crossed the threshold. + + // transform alice account in multisign with bob and carol as authorities + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + multi_action::init_gov(alice); + // Ths is a simple multi_action: there is no capability being stored + multi_action::init_type(alice, false); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // bob create a proposal + let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); + let id = multi_action::propose_new(bob, alice_address, proposal); + let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, alice_address); + assert!(passed == false, 7357001); + option::destroy_none(cap_opt); + + let (passed, cap_opt) = multi_action::vote_with_id(carol, &id, alice_address); + assert!(passed == true, 7357002); + // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED + assert!(option::is_none(&cap_opt), 7357003); + + option::destroy_none(cap_opt); + } + + // Happy day: complete vote action with withdraw capability + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun vote_action_happy_withdraw_cap(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + // Scenario: testing that a payment type multisig could be created with this module: that the WithdrawCapability can be used here. + + let _vals = mock::genesis_n_vals(root, 4); + mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); + let alice_address = @0x1000a; + + // fund the alice multi_action's account + ol_account::transfer(alice, alice_address, 100); + + // make the bob and carol the signers on the alice safe, and 2-of-2 need to sign + multi_action::init_gov(alice); + multi_action::init_type(alice, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::none()); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // bob create a proposal and vote + let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); + let id = multi_action::propose_new(bob, alice_address, proposal); + let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, alice_address); + assert!(passed == false, 7357001); + option::destroy_none(cap_opt); + + // carol vote on bob proposal + let (passed, cap_opt) = multi_action::vote_with_id(carol, &id, alice_address); + assert!(passed == true, 7357002); + + // THE WITHDRAW CAPABILITY IS WHERE WE EXPECT + assert!(option::is_some(&cap_opt), 7357003); + let cap = option::extract(&mut cap_opt); + let c = ol_account::withdraw_with_capability( + &cap, + 42, + ); + // deposit to erik account + ol_account::create_account(root, @0x1000e); + ol_account::deposit_coins(@0x1000e, c); + option::fill(&mut cap_opt, cap); + // check erik account balance + let (_, balance) = ol_account::balance(@0x1000e); + assert!(balance == 42, 7357004); + + multi_action::maybe_restore_withdraw_cap(cap_opt); + } + + // Try to vote on a closed ballot + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x30012, location = ol_framework::multi_action)] + fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, dave: &signer) { + // Scenario: Testing that if an action expires voting cannot be done. + + let _vals = mock::genesis_n_vals(root, 3); + mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); + + // we are at epoch 0 + let epoch = reconfiguration::get_current_epoch(); + assert!(epoch == 0, 7357001); + + // dave creates erik resource account. He is not one of the validators, and is not an authority in the multisig. + let (erik, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1000e"); + let erik_address = signer::address_of(&erik); + assert!(resource_account::is_resource_account(erik_address), 7357002); + + // fund the account + ol_account::transfer(alice, erik_address, 100); + // offer alice and bob authority on the safe + safe::init_payment_multisig(&erik); // both need to sign + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(&erik, authorities, option::none()); + multi_action::claim_offer(alice, erik_address); + multi_action::claim_offer(bob, erik_address); + multi_action::finalize_and_cage2(&erik); + + // make a proposal for governance, expires in 2 epoch from now + let id = multi_action::propose_governance(alice, erik_address, vector::empty(), true, option::some(1), option::some(2)); + + mock::trigger_epoch(root); // epoch 1 + mock::trigger_epoch(root); // epoch 2 + mock::trigger_epoch(root); // epoch 3 -- voting ended here + mock::trigger_epoch(root); // epoch 4 -- now expired + + let epoch = reconfiguration::get_current_epoch(); + assert!(epoch == 4, 7357003); + + // trying to vote on a closed ballot will error + let _passed = multi_action::vote_governance(bob, erik_address, &id); + } + + + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun governance_change_auths(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + // Scenario: The multisig gets initiated with the 2 validators as the only authorities. IT takes 2-of-2 to sign. + // later they add a third (Dave) so it becomes a 2-of-3. + // Dave and Bob, then remove alice so it becomes 2-of-2 again + + let _vals = mock::genesis_n_vals(root, 4); + mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); + let carol_address = @0x1000c; + let dave_address = @0x1000d; + + // fund the account + ol_account::transfer(alice, carol_address, 100); + // offer alice and bob authority on the safe + multi_action::init_gov(carol);// both need to sign + multi_action::init_type(carol, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + multi_action::finalize_and_cage2(carol); + + // alice is going to propose to change the authorities to add dave + let id = multi_action::propose_governance(alice, carol_address, + vector::singleton(dave_address), true, option::none(), + option::none()); + + // check authorities did not change + let ret = multi_action::get_authorities(carol_address); + assert!(ret == authorities, 7357001); + + // bob votes. bob could either use vote_governance() + let passed = multi_action::vote_governance(bob, carol_address, &id); + assert!(passed, 7357002); + + // check authorities did not change + let ret = multi_action::get_authorities(carol_address); + assert!(ret == authorities, 7357003); + + // check the Offer + let ret = multi_action::get_offer_proposed(carol_address); + assert!(ret == vector::singleton(dave_address), 7357004); + + // dave claims the offer and it becomes final. + multi_action::claim_offer(dave, carol_address); + + // Chek new set of authorities + let ret = multi_action::get_authorities(carol_address); + vector::push_back(&mut authorities, dave_address); + assert!(ret == authorities, 7357005); + + // Check if offer was cleaned + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357006); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357007); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357008); + + // Now dave and bob, will conspire to remove alice. + // NOTE: `false` means `remove account` here + let id = multi_action::propose_governance(dave, carol_address, vector::singleton(signer::address_of(alice)), false, option::none(), option::none()); + let a = multi_action::get_authorities(carol_address); + assert!(vector::length(&a) == 3, 7357009); // no change yet + + // bob votes and it becomes final. Bob could either use vote_governance() + let passed = multi_action::vote_governance(bob, carol_address, &id); + assert!(passed, 7357008); + let a = multi_action::get_authorities(carol_address); + assert!(vector::length(&a) == 2, 73570010); + assert!(!multi_action::is_authority(carol_address, signer::address_of(alice)), 7357011); + + // Check if offer was cleaned + assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 73570012); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 73570013); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 73570014); + } + + // Happy day: change the threshold of a multisig + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun governance_change_threshold(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + // Scenario: The multisig gets initiated with the 2 bob and carol as the only authorities. It takes 2-of-2 to sign. + // They decide next only 1-of-2 will be needed. + + let _vals = mock::genesis_n_vals(root, 3); + mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); + + // Dave creates the resource account. He is not one of the validators, and is not an authority in the multisig. + let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); + let new_resource_address = signer::address_of(&resource_sig); + assert!(resource_account::is_resource_account(new_resource_address), 7357001); + + // fund the account + ol_account::transfer(alice, new_resource_address, 100); + + // offer bob and carol authority on the safe + multi_action::init_gov(&resource_sig);// both need to sign + multi_action::init_type(&resource_sig, false); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(&resource_sig, authorities, option::none()); + multi_action::claim_offer(carol, new_resource_address); + multi_action::claim_offer(bob, new_resource_address); + multi_action::finalize_and_cage2(&resource_sig); + + // carol is going to propose to change the authorities to add Rando + let id = multi_action::propose_governance(carol, new_resource_address, vector::empty(), true, option::some(1), option::none()); + + // check authorities and threshold + let a = multi_action::get_authorities(new_resource_address); + assert!(vector::length(&a) == 2, 7357002); // no change + let (n, _m) = multi_action::get_threshold(new_resource_address); + assert!(n == 2, 7357003); + + // bob votes and it becomes final. Bob could either use vote_governance() + let passed = multi_action::vote_governance(bob, new_resource_address, &id); + + // check authorities and threshold + assert!(passed, 7357004); + let a = multi_action::get_authorities(new_resource_address); + assert!(vector::length(&a) == 2, 7357005); // no change + let (n, _m) = multi_action::get_threshold(new_resource_address); + assert!(n == 1, 7357006); + + // now any other type of action can be taken with just one signer + let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); + let id = multi_action::propose_new(bob, new_resource_address, proposal); + let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, new_resource_address); + assert!(passed == true, 7357007); + + // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED + assert!(option::is_none(&cap_opt), 7357008); + + option::destroy_none(cap_opt); + } + + // Vote new athority before the previous one is claimed + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, erik = @0x1000e)] + fun governance_vote_before_claim(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 5); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add dave + let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), true, option::none(), option::none()); + + // bob votes and dave does not claims the offer + multi_action::vote_governance(bob, alice_address, &id); + + // check authorities and threshold + assert!(multi_action::get_authorities(alice_address) == authorities, 7357001); + let (n, _m) = multi_action::get_threshold(alice_address); + assert!(n == 2, 7357002); + + // check offer + print(&multi_action::get_offer_proposed(alice_address)); + assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357003); + print(&multi_action::get_offer_proposed(alice_address)); + assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000d), 7357004); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(7), 7357005); + + mock::trigger_epoch(root); // epoch 1 + + // bob is going to propose to change the authorities to add erik + let id = multi_action::propose_governance(bob, alice_address, vector::singleton(@0x1000e), true, option::none(), option::none()); + + // carol votes + multi_action::vote_governance(carol, alice_address, &id); + + // check authorities and threshold + assert!(multi_action::get_authorities(alice_address) == authorities, 7357001); + let (n, _m) = multi_action::get_threshold(alice_address); + assert!(n == 2, 7357002); + + // check offer + assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357003); + let proposed = vector::singleton(@0x1000d); + vector::push_back(&mut proposed, @0x1000e); + assert!(multi_action::get_offer_proposed(alice_address) == proposed, 7357004); + let expiration = vector::singleton(7); + vector::push_back(&mut expiration, 8); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == expiration, 7357005); + } + + // Try to vote an invalid address for new authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x60018, location = ol_framework::multisig_account)] + fun governance_vote_invalid_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add dave + let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0xCAFE), true, option::none(), option::none()); + + // bob votes and dave does not claims the offer + multi_action::vote_governance(bob, alice_address, &id); + } + + // Try to vote duplicated addresses for new authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x10001, location = ol_framework::multisig_account)] + fun governance_vote_duplicated_addresses(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 5); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add dave twice + let authorities = vector::singleton(@0x1000d); + vector::push_back(&mut authorities, @0x1000d); + let _id = multi_action::propose_governance(carol, alice_address, authorities, true, option::none(), option::none()); + } + + // Try to vote multisig account address for new authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x10013, location = ol_framework::multisig_account)] + fun governance_vote_multisig_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 5); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add dave twice + let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(alice_address), true, option::none(), option::none()); + } + + // Try to vote an owner as new authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x10026, location = ol_framework::multi_action)] + fun governance_vote_owner_as_new_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to change the authorities to add bob + let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(signer::address_of(bob)), true, option::none(), option::none()); + } + + // Try to vote remove an authority that is not in the multisig + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + #[expected_failure(abort_code = 0x60027, location = ol_framework::multi_action)] + fun governance_vote_remove_non_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + let _vals = mock::genesis_n_vals(root, 3); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage2(alice); + + // carol is going to propose to remove dave + let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), false, option::none(), option::none()); + } } diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index db2a7316a..1f86ff7b3 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -23,801 +23,739 @@ //V7 NOTE: from V6 we are refactoring so the the account first needs to be created as a "resource account". It's a minor change given that V6 had a similar construct of a "signerless account", Previously in ol this meant to "Brick" the authkey after the WithdrawCapability was stored in a common struct. Vendor had independenly made the same design using Signer Capability. module ol_framework::multi_action { - use std::vector; - use std::option::{Self, Option}; - use std::signer; - use std::error; - use std::guid; - use diem_framework::create_signer::create_signer; - use diem_framework::account::{Self, WithdrawCapability}; - use diem_framework::multisig_account; - use ol_framework::ballot::{Self, BallotTracker}; - use ol_framework::epoch_helper; - - // use diem_std::debug::print; - - friend ol_framework::community_wallet_init; - friend ol_framework::donor_voice_txs; - friend ol_framework::safe; - - #[test_only] - friend ol_framework::test_multi_action; - - const EGOV_NOT_INITIALIZED: u64 = 0x1; - /// The owner of this account can't be an authority, since it will subsequently be bricked. The signer of this account is no longer useful. The account is now controlled by the Governance logic. - const ESIGNER_CANT_BE_AUTHORITY: u64 = 0x2; - /// signer not authorized to approve a transaction. - const ENOT_AUTHORIZED: u64 = 0x3; - /// There are no pending transactions to search - const EPENDING_EMPTY: u64 = 4; - /// Not enough signers configured - const ENO_SIGNERS: u64 = 5; - /// The multisig setup is not finalized, the sponsor needs to brick their authkey. The account setup sponsor needs to be verifiably locked out before operations can begin. - const ENOT_FINALIZED_NOT_BRICK: u64 = 6; - /// Already registered this action type - const EACTION_ALREADY_EXISTS: u64 = 7; - /// Action not found - const EACTION_NOT_FOUND: u64 = 8; - /// Proposal is expired - const EPROPOSAL_EXPIRED: u64 = 9; - /// Proposal is expired - const EDUPLICATE_PROPOSAL: u64 = 10; - /// Proposal is expired - const EPROPOSAL_NOT_FOUND: u64 = 11; - /// Proposal voting is closed - const EVOTING_CLOSED: u64 = 0x12; - /// No addresses in multisig changes - const EEMPTY_ADDRESSES: u64 = 13; - /// Duplicate vote - const EDUPLICATE_VOTE: u64 = 14; - /// Offer expired - const EOFFER_EXPIRED: u64 = 0x15; - /// Offer empty - const EOFFER_EMPTY: u64 = 0x16; - /// Not offered to initial authorities - const ENOT_OFFERED: u64 = 0x17; - /// Not enough claimed authorities - const ENOT_ENOUGH_CLAIMED: u64 = 0x18; - /// Account is already a multisig - const EALREADY_MULTISIG: u64 = 0x19; - /// Address not proposed for authority role - const EADDRESS_NOT_PROPOSED: u64 = 0x20; - /// Address proposed for authority role does not exist - const EPROPOSED_NOT_EXISTS: u64 = 0x21; - /// Offer duration must be greater than zero - const EZERO_DURATION: u64 = 0x22; - /// Offer already claimed - const EALREADY_CLAIMED: u64 = 0x23; - /// Too many addresses in offer - avoid DoS attack - const ETOO_MANY_ADDRESSES: u64 = 0x24; - /// Offer already exists - const EOFFER_ALREADY_EXISTS: u64 = 0x25; - /// Already an owner - const EALREADY_OWNER: u64 = 0x26; - /// Owner not found - const EOWNER_NOT_FOUND: u64 = 0x27; - - /// default setting for a proposal to expire - const DEFAULT_EPOCHS_EXPIRE: u64 = 14; - /// default setting for an offer to expire - const DEFAULT_EPOCHS_OFFER_EXPIRE: u64 = 7; - /// minimum number of claimed authorities to cage the account - const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 2; - /// maximum number of address to offer - const MAX_OFFER_ADDRESSES: u64 = 10; - - /// A Governance account is an account which requires multiple votes from Authorities to send a transaction. - /// A multisig can be used to get agreement on different types of Actions, such as a payment transaction where the handler code for the transaction is an a separate contract. See for example MultiSigPayment. - /// Governance struct holds the metadata for all the instances of Actions on this account. - /// Every action has the same set of authorities and governance. - /// This is intentional, since privilege escalation can happen if each action has a different set of governance, but access to funds and other state. - /// If the organization wishes to have Actions with different governance, then a separate Account is necessary. - - - /// DANGER - /// Governance optionally holds a WithdrawCapability, which is used to withdraw funds from the account. All actions share the same WithdrawCapability. - /// The WithdrawCapability can be used to withdraw funds from the account. - /// Ordinarily only the signer/owner of this address can use it. - /// We are bricking the signer, and as such the withdraw capability is now controlled by the Governance logic. - /// Core Devs: This is a major attack vector. The WithdrawCapability should NEVER be returned to a public caller, UNLESS it is within the vote and approve flow. - - /// Note, the WithdrawCapability is moved to this shared structure, and as such the signer of the account is bricked. The signer who was the original owner of this account ("sponsor") can no longer issue transactions to this account, and as such the WithdrawCapability would be inaccessible. So on initialization we extract the WithdrawCapability into the Governance governance struct. - - //TODO: feature: signers is a hashmap and each can have a different weight - struct Governance has key { - cfg_duration_epochs: u64, - cfg_default_n_sigs: u64, - signers: vector
, - withdraw_capability: Option, // for the calling function to be able to do asset moving operations. - guid_capability: account::GUIDCapability, // this is needed to create GUIDs for the Ballot. - } - - struct Action has key, store { - can_withdraw: bool, - vote: BallotTracker>, - } - - // All proposals share some common fields - // and each proposal can add type-specific parameters - // The handler for such specific parameters needs to included in code by an external contract. - // Governance, will only say if it passed or not. - // Note: The underlying Ballot deals with the GUID generation - struct Proposal has store, drop { - // The transaction to be executed - proposal_data: ProposalData, - // The votes received - votes: vector
, - // approved - approved: bool, - // The expiration time for the transaction - expiration_epoch: u64, - } - - /// Offer struct to manage the proposal and claiming of new authorities. - /// - proposed: List of authority addresses proposed - /// - claimed: List of authority addresses that have claimed the offer. - /// - expiration_epoch: The epoch when each proposed expires. - struct Offer has key, store { - proposed: vector
, - claimed: vector
, - expiration_epoch: vector, - } - - fun construct_empty_offer(): Offer { - Offer { - proposed: vector::empty(), - claimed: vector::empty(), - expiration_epoch: vector::empty(), + use std::vector; + use std::option::{Self, Option}; + use std::signer; + use std::error; + use std::guid; + use diem_framework::create_signer::create_signer; + use diem_framework::account::{Self, WithdrawCapability}; + use diem_framework::multisig_account; + use ol_framework::ballot::{Self, BallotTracker}; + use ol_framework::epoch_helper; + + // use diem_std::debug::print; + + friend ol_framework::community_wallet_init; + friend ol_framework::donor_voice_txs; + friend ol_framework::safe; + + #[test_only] + friend ol_framework::test_multi_action; + + const EGOV_NOT_INITIALIZED: u64 = 0x1; + /// The owner of this account can't be an authority, since it will subsequently be bricked. The signer of this account is no longer useful. The account is now controlled by the Governance logic. + const ESIGNER_CANT_BE_AUTHORITY: u64 = 0x2; + /// signer not authorized to approve a transaction. + const ENOT_AUTHORIZED: u64 = 0x3; + /// There are no pending transactions to search + const EPENDING_EMPTY: u64 = 4; + /// Not enough signers configured + const ENO_SIGNERS: u64 = 5; + /// The multisig setup is not finalized, the sponsor needs to brick their authkey. The account setup sponsor needs to be verifiably locked out before operations can begin. + const ENOT_FINALIZED_NOT_BRICK: u64 = 6; + /// Already registered this action type + const EACTION_ALREADY_EXISTS: u64 = 7; + /// Action not found + const EACTION_NOT_FOUND: u64 = 8; + /// Proposal is expired + const EPROPOSAL_EXPIRED: u64 = 9; + /// Proposal is expired + const EDUPLICATE_PROPOSAL: u64 = 10; + /// Proposal is expired + const EPROPOSAL_NOT_FOUND: u64 = 11; + /// Proposal voting is closed + const EVOTING_CLOSED: u64 = 0x12; + /// No addresses in multisig changes + const EEMPTY_ADDRESSES: u64 = 13; + /// Duplicate vote + const EDUPLICATE_VOTE: u64 = 14; + /// Offer expired + const EOFFER_EXPIRED: u64 = 0x15; + /// Offer empty + const EOFFER_EMPTY: u64 = 0x16; + /// Not offered to initial authorities + const ENOT_OFFERED: u64 = 0x17; + /// Not enough claimed authorities + const ENOT_ENOUGH_CLAIMED: u64 = 0x18; + /// Account is already a multisig + const EALREADY_MULTISIG: u64 = 0x19; + /// Address not proposed for authority role + const EADDRESS_NOT_PROPOSED: u64 = 0x20; + /// Address proposed for authority role does not exist + const EPROPOSED_NOT_EXISTS: u64 = 0x21; + /// Offer duration must be greater than zero + const EZERO_DURATION: u64 = 0x22; + /// Offer already claimed + const EALREADY_CLAIMED: u64 = 0x23; + /// Too many addresses in offer - avoid DoS attack + const ETOO_MANY_ADDRESSES: u64 = 0x24; + /// Offer already exists + const EOFFER_ALREADY_EXISTS: u64 = 0x25; + /// Already an owner + const EALREADY_OWNER: u64 = 0x26; + /// Owner not found + const EOWNER_NOT_FOUND: u64 = 0x27; + + /// default setting for a proposal to expire + const DEFAULT_EPOCHS_EXPIRE: u64 = 14; + /// default setting for an offer to expire + const DEFAULT_EPOCHS_OFFER_EXPIRE: u64 = 7; + /// minimum number of claimed authorities to cage the account + const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 2; + /// maximum number of address to offer + const MAX_OFFER_ADDRESSES: u64 = 10; + + /// A Governance account is an account which requires multiple votes from Authorities to send a transaction. + /// A multisig can be used to get agreement on different types of Actions, such as a payment transaction where the handler code for the transaction is an a separate contract. See for example MultiSigPayment. + /// Governance struct holds the metadata for all the instances of Actions on this account. + /// Every action has the same set of authorities and governance. + /// This is intentional, since privilege escalation can happen if each action has a different set of governance, but access to funds and other state. + /// If the organization wishes to have Actions with different governance, then a separate Account is necessary. + + + /// DANGER + /// Governance optionally holds a WithdrawCapability, which is used to withdraw funds from the account. All actions share the same WithdrawCapability. + /// The WithdrawCapability can be used to withdraw funds from the account. + /// Ordinarily only the signer/owner of this address can use it. + /// We are bricking the signer, and as such the withdraw capability is now controlled by the Governance logic. + /// Core Devs: This is a major attack vector. The WithdrawCapability should NEVER be returned to a public caller, UNLESS it is within the vote and approve flow. + + /// Note, the WithdrawCapability is moved to this shared structure, and as such the signer of the account is bricked. The signer who was the original owner of this account ("sponsor") can no longer issue transactions to this account, and as such the WithdrawCapability would be inaccessible. So on initialization we extract the WithdrawCapability into the Governance governance struct. + + //TODO: feature: signers is a hashmap and each can have a different weight + struct Governance has key { + cfg_duration_epochs: u64, + cfg_default_n_sigs: u64, + signers: vector
, + withdraw_capability: Option, // for the calling function to be able to do asset moving operations. + guid_capability: account::GUIDCapability, // this is needed to create GUIDs for the Ballot. } - } - - fun clean_offer(addr: address) acquires Offer { - let offer = borrow_global_mut(addr); - offer.proposed = vector::empty(); - offer.claimed = vector::empty(); - offer.expiration_epoch = vector::empty(); - } - - // Initialize the governance structs for this account. - // Governance contains the constraints for each Action that are checked on each vote (n_sigs, expiration, signers, etc) - // Also, an initial Action of type PropGovSigners is created, which is used to govern the signers and threshold for this account. - public(friend) fun init_gov(sig: &signer) { - // heals un-initialized state, and does nothing if state already exists. - - let multisig_address = signer::address_of(sig); - // User footgun. The signer of this account is bricked, and as such the signer can no longer be an authority. - - if (!exists(multisig_address)) { - move_to(sig, Governance { - cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, - cfg_default_n_sigs: 0, // deprecate - signers: vector::empty(), - withdraw_capability: option::none(), - guid_capability: account::create_guid_capability(sig), - }); - }; - - if (!exists>(multisig_address)) { - move_to(sig, Action { - can_withdraw: false, - vote: ballot::new_tracker>(), - }); - }; - - if (!exists(multisig_address)) { - move_to(sig, construct_empty_offer()); - }; - } - - // Private function to assist governance vote - fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { - let offer = borrow_global_mut(addr); - let duration = epoch_helper::get_current_epoch() + DEFAULT_EPOCHS_OFFER_EXPIRE; - let i = 0; - while (i < vector::length(&proposed)) { - let addr = vector::borrow(&proposed, i); - vector::push_back(&mut offer.proposed, *addr); - vector::push_back(&mut offer.expiration_epoch, duration); - i = i + 1; - }; - } - - // DANGER - may forge the signer of the multisig account is necessary here - // TODO: remove this function after offer migration is completed - // Migrate a legacy account to have structure Offer in order to propose authorities changes - public entry fun migrate_offer(sig: &signer, multisig_address: address) { - // Ensure the account does not have Offer structure - assert!(!exists_offer(multisig_address), error::already_exists(EOFFER_ALREADY_EXISTS)); - - // if account is multisig, forge signer and add Offer to the multisig account - if (multisig_account::is_multisig(multisig_address)) { - // a) multisig account: ensure the signer is in the authorities list - assert!(is_authority(multisig_address, signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); - - // We create the signer for the multisig account here since this is required - // to add the Offer resource. - // This should be safe because we check that the signer is in the authorities list. - // Also, after all accounts are migrated this function will be deprecated. - let multisig_signer = &create_signer(multisig_address); // <<< DANGER - - // create Offer structure - let offer = construct_empty_offer(); - move_to(multisig_signer, offer); - } else { - // b) initiated account: ensure the account is initialized with governance and add Offer to the account - assert!(multisig_address == signer::address_of(sig), error::permission_denied(ENOT_AUTHORIZED)); - assert!(is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); - let offer = construct_empty_offer(); - move_to(sig, offer); - }; - } - - fun ensure_valid_propose_offer_state(addr: address) { - // Ensure the account is not yet initialized as multisig - assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); - - // Ensure the account has governance initialized and offer structure - assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); - assert!(exists_offer(addr), error::already_exists(EOFFER_ALREADY_EXISTS)); - } - - fun ensure_valid_propose_offer_params(addr: address, proposed: vector
, duration_epochs: Option) { - - // Ensure the proposed list is not empty - assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); - - // Ensure the proposed list is not greater than the maximum limit - avoid DoS attack - assert!(vector::length(&proposed) <= MAX_OFFER_ADDRESSES, error::invalid_argument(ETOO_MANY_ADDRESSES)); - - // Ensure distinct addresses and multisign owner not in the list - multisig_account::validate_owners(&proposed, addr); - - if (option::is_some(&duration_epochs)) { - let duration_epochs = *option::borrow(&duration_epochs); - // Ensure duration is greater than zero - assert!(duration_epochs > 0, error::invalid_argument(EZERO_DURATION)); - }; - } - - // Calculate the expiration epoch for the offer. - fun calculate_expiration_epoch(duration_epochs: Option): u64 { - let duration_epochs = if (option::is_some(&duration_epochs)) { - *option::borrow(&duration_epochs) - } else { - DEFAULT_EPOCHS_OFFER_EXPIRE - }; - - epoch_helper::get_current_epoch() + duration_epochs - } - - // Update the offer with the new proposed authorities and expiration epoch. - fun update_offer(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { - let expiration_epoch = calculate_expiration_epoch(duration_epochs); - - // Update offer - let offer = borrow_global_mut(addr); - - // Remove claimed addresses that are not in the new proposed list - let j = 0; - while (j < vector::length(&offer.claimed)) { - let claimed_addr = vector::borrow(&offer.claimed, j); - if (!vector::contains(&proposed, claimed_addr)) { - vector::remove(&mut offer.claimed, j); - } else { - j = j + 1; - }; - }; - - // Remove new proposed addresses that are already claimed - let i = 0; - while (i < vector::length(&proposed)) { - let proposed_addr = vector::borrow(&proposed, i); - if (vector::contains(&offer.claimed, proposed_addr)) { - vector::remove(&mut proposed, i); - }; - i = i + 1; - }; - - // Remove old proposed addresses that are not in the new proposed list - let j = 0; - while (j < vector::length(&offer.proposed)) { - let proposed_addr = vector::borrow(&offer.proposed, j); - if (!vector::contains(&proposed, proposed_addr)) { - vector::remove(&mut offer.proposed, j); - vector::remove(&mut offer.expiration_epoch, j); - } else { - j = j + 1; - }; - }; - - // Insert/Update proposed and expiration epoch lists - let k = 0; - while (k < vector::length(&proposed)) { - // if already contains the address, update the expiration_epoch - let proposed_addr = vector::borrow(&proposed, k); - let (found, i) = vector::index_of(&offer.proposed, proposed_addr); - if (found) { + + struct Action has key, store { + can_withdraw: bool, + vote: BallotTracker>, + } + + // All proposals share some common fields + // and each proposal can add type-specific parameters + // The handler for such specific parameters needs to included in code by an external contract. + // Governance, will only say if it passed or not. + // Note: The underlying Ballot deals with the GUID generation + struct Proposal has store, drop { + // The transaction to be executed + proposal_data: ProposalData, + // The votes received + votes: vector
, + // approved + approved: bool, + // The expiration time for the transaction + expiration_epoch: u64, + } + + /// Offer struct to manage the proposal and claiming of new authorities. + /// - proposed: List of authority addresses proposed + /// - claimed: List of authority addresses that have claimed the offer. + /// - expiration_epoch: The epoch when each proposed expires. + struct Offer has key, store { + proposed: vector
, + claimed: vector
, + expiration_epoch: vector, + } + + fun construct_empty_offer(): Offer { + Offer { + proposed: vector::empty(), + claimed: vector::empty(), + expiration_epoch: vector::empty(), + } + } + + fun clean_offer(addr: address) acquires Offer { + let offer = borrow_global_mut(addr); + offer.proposed = vector::empty(); + offer.claimed = vector::empty(); + offer.expiration_epoch = vector::empty(); + } + + // Initialize the governance structs for this account. + // Governance contains the constraints for each Action that are checked on each vote (n_sigs, expiration, signers, etc) + // Also, an initial Action of type PropGovSigners is created, which is used to govern the signers and threshold for this account. + public(friend) fun init_gov(sig: &signer) { + // heals un-initialized state, and does nothing if state already exists. + + let multisig_address = signer::address_of(sig); + // User footgun. The signer of this account is bricked, and as such the signer can no longer be an authority. + + if (!exists(multisig_address)) { + move_to(sig, Governance { + cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, + cfg_default_n_sigs: 0, // deprecate + signers: vector::empty(), + withdraw_capability: option::none(), + guid_capability: account::create_guid_capability(sig), + }); + }; + + if (!exists>(multisig_address)) { + move_to(sig, Action { + can_withdraw: false, + vote: ballot::new_tracker>(), + }); + }; + + if (!exists(multisig_address)) { + move_to(sig, construct_empty_offer()); + }; + } + + // Private function to assist governance vote + fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { + let offer = borrow_global_mut(addr); + let duration = epoch_helper::get_current_epoch() + DEFAULT_EPOCHS_OFFER_EXPIRE; + let i = 0; + while (i < vector::length(&proposed)) { + let addr = vector::borrow(&proposed, i); + vector::push_back(&mut offer.proposed, *addr); + vector::push_back(&mut offer.expiration_epoch, duration); + i = i + 1; + }; + } + + // DANGER - may forge the signer of the multisig account is necessary here + // TODO: remove this function after offer migration is completed + // Migrate a legacy account to have structure Offer in order to propose authorities changes + public entry fun migrate_offer(sig: &signer, multisig_address: address) { + // Ensure the account does not have Offer structure + assert!(!exists_offer(multisig_address), error::already_exists(EOFFER_ALREADY_EXISTS)); + + // if account is multisig, forge signer and add Offer to the multisig account + if (multisig_account::is_multisig(multisig_address)) { + // a) multisig account: ensure the signer is in the authorities list + assert!(is_authority(multisig_address, signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); + + // We create the signer for the multisig account here since this is required + // to add the Offer resource. + // This should be safe because we check that the signer is in the authorities list. + // Also, after all accounts are migrated this function will be deprecated. + let multisig_signer = &create_signer(multisig_address); // <<< DANGER + + // create Offer structure + let offer = construct_empty_offer(); + move_to(multisig_signer, offer); + } else { + // b) initiated account: ensure the account is initialized with governance and add Offer to the account + assert!(multisig_address == signer::address_of(sig), error::permission_denied(ENOT_AUTHORIZED)); + assert!(is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); + let offer = construct_empty_offer(); + move_to(sig, offer); + }; + } + + fun ensure_valid_propose_offer_state(addr: address) { + // Ensure the account is not yet initialized as multisig + assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); + + // Ensure the account has governance initialized and offer structure + assert!(is_gov_init(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + assert!(exists_offer(addr), error::already_exists(EOFFER_ALREADY_EXISTS)); + } + + fun ensure_valid_propose_offer_params(addr: address, proposed: vector
, duration_epochs: Option) { + + // Ensure the proposed list is not empty + assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); + + // Ensure the proposed list is not greater than the maximum limit - avoid DoS attack + assert!(vector::length(&proposed) <= MAX_OFFER_ADDRESSES, error::invalid_argument(ETOO_MANY_ADDRESSES)); + + // Ensure distinct addresses and multisign owner not in the list + multisig_account::validate_owners(&proposed, addr); + + if (option::is_some(&duration_epochs)) { + let duration_epochs = *option::borrow(&duration_epochs); + // Ensure duration is greater than zero + assert!(duration_epochs > 0, error::invalid_argument(EZERO_DURATION)); + }; + } + + // Calculate the expiration epoch for the offer. + fun calculate_expiration_epoch(duration_epochs: Option): u64 { + let duration_epochs = if (option::is_some(&duration_epochs)) { + *option::borrow(&duration_epochs) + } else { + DEFAULT_EPOCHS_OFFER_EXPIRE + }; + + epoch_helper::get_current_epoch() + duration_epochs + } + + // Update the offer with the new proposed authorities and expiration epoch. + fun update_offer(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { + let expiration_epoch = calculate_expiration_epoch(duration_epochs); + + // Update offer + let offer = borrow_global_mut(addr); + + // Remove claimed addresses that are not in the new proposed list + let j = 0; + while (j < vector::length(&offer.claimed)) { + let claimed_addr = vector::borrow(&offer.claimed, j); + if (!vector::contains(&proposed, claimed_addr)) { + vector::remove(&mut offer.claimed, j); + } else { + j = j + 1; + }; + }; + + // Remove new proposed addresses that are already claimed + let i = 0; + while (i < vector::length(&proposed)) { + let proposed_addr = vector::borrow(&proposed, i); + if (vector::contains(&offer.claimed, proposed_addr)) { + vector::remove(&mut proposed, i); + }; + i = i + 1; + }; + + // Remove old proposed addresses that are not in the new proposed list + let j = 0; + while (j < vector::length(&offer.proposed)) { + let proposed_addr = vector::borrow(&offer.proposed, j); + if (!vector::contains(&proposed, proposed_addr)) { + vector::remove(&mut offer.proposed, j); + vector::remove(&mut offer.expiration_epoch, j); + } else { + j = j + 1; + }; + }; + + // Insert/Update proposed and expiration epoch lists + let k = 0; + while (k < vector::length(&proposed)) { + // if already contains the address, update the expiration_epoch + let proposed_addr = vector::borrow(&proposed, k); + let (found, i) = vector::index_of(&offer.proposed, proposed_addr); + if (found) { + vector::remove(&mut offer.expiration_epoch, i); + vector::insert(&mut offer.expiration_epoch, i, expiration_epoch); + } else { + vector::push_back(&mut offer.proposed, *proposed_addr); + vector::push_back(&mut offer.expiration_epoch, expiration_epoch); + }; + k = k + 1; + }; + } + + // Propose an offer to new authorities on the signer account + // or update the expiration epoch of the existing proposed authorities. + // - sig: The signer proposing the offer. + // - proposed: The list of authorities addresses proposed. + // - duration_epochs: The duration in epochs before the offer expires. + public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + // Propose the offer on the signer's account + let addr = signer::address_of(sig); + ensure_valid_propose_offer_state(addr); + ensure_valid_propose_offer_params(addr, proposed, duration_epochs); + update_offer(addr, proposed, duration_epochs); + } + + // Allows a proposed authority to claim their offer. + // - sig: The signer making the claim. + // - multisig_address: The address of the multisig account. + public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer, Governance { + let sender_addr = signer::address_of(sig); + + // Ensure the account has an offer + assert!(exists_offer(multisig_address), error::not_found(ENOT_OFFERED)); + + // Ensure the offer has not expired + assert!(!is_offer_expired(multisig_address, sender_addr), error::out_of_range(EOFFER_EXPIRED)); + + let offer = borrow_global_mut(multisig_address); + + // Ensure the sender is not in the claimed list + assert!(!vector::contains(&offer.claimed, &sender_addr), error::already_exists(EALREADY_CLAIMED)); + + // Ensure the sender is in the proposed list + assert!(vector::contains(&offer.proposed, &sender_addr), error::not_found(EADDRESS_NOT_PROPOSED)); + + // Remove the sender from the proposed list and expiration_epoch + let (_, i) = vector::index_of(&offer.proposed, &sender_addr); + vector::remove(&mut offer.proposed, i); vector::remove(&mut offer.expiration_epoch, i); - vector::insert(&mut offer.expiration_epoch, i, expiration_epoch); - } else { - vector::push_back(&mut offer.proposed, *proposed_addr); - vector::push_back(&mut offer.expiration_epoch, expiration_epoch); - }; - k = k + 1; - }; - } - - // Propose an offer to new authorities on the signer account - // or update the expiration epoch of the existing proposed authorities. - // - sig: The signer proposing the offer. - // - proposed: The list of authorities addresses proposed. - // - duration_epochs: The duration in epochs before the offer expires. - public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { - // Propose the offer on the signer's account - let addr = signer::address_of(sig); - ensure_valid_propose_offer_state(addr); - ensure_valid_propose_offer_params(addr, proposed, duration_epochs); - update_offer(addr, proposed, duration_epochs); - } - - // Allows a proposed authority to claim their offer. - // - sig: The signer making the claim. - // - multisig_address: The address of the multisig account. - public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer, Governance { - let sender_addr = signer::address_of(sig); - - // Ensure the account has an offer - assert!(exists_offer(multisig_address), error::not_found(ENOT_OFFERED)); - - // Ensure the offer has not expired - assert!(!is_offer_expired(multisig_address, sender_addr), error::out_of_range(EOFFER_EXPIRED)); - - let offer = borrow_global_mut(multisig_address); - - // Ensure the sender is not in the claimed list - assert!(!vector::contains(&offer.claimed, &sender_addr), error::already_exists(EALREADY_CLAIMED)); - - // Ensure the sender is in the proposed list - assert!(vector::contains(&offer.proposed, &sender_addr), error::not_found(EADDRESS_NOT_PROPOSED)); - - // Remove the sender from the proposed list and expiration_epoch - let (_, i) = vector::index_of(&offer.proposed, &sender_addr); - vector::remove(&mut offer.proposed, i); - vector::remove(&mut offer.expiration_epoch, i); - - if (multisig_account::is_multisig(multisig_address)) { - // a) finalized account: add authority to the multisig account - let ms = borrow_global_mut(multisig_address); - maybe_update_authorities(ms, true, &vector::singleton(sender_addr)); - if (vector::length(&offer.proposed) == 0) { - // clean the Offer - clean_offer(multisig_address); - }; - } else { - // b) initiated account: add sender to the claimed list - vector::push_back(&mut offer.claimed, sender_addr); - }; - } - - /// Finalizes the multisign account and locks it (cage). - /// - sig: The signer finalizing the account. - /// Aborts if governance is not initialized, the account is already a multisig, - /// there are not enough claimed authorities, or the offer is not found. - public fun finalize_and_cage2(sig: &signer) acquires Offer { - let addr = signer::address_of(sig); - - // check it is not yet initialized - assert!(!multisig_account::is_multisig(addr), error::already_exists(EALREADY_MULTISIG)); - - // check governance - assert!(exists(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); - assert!(exists>(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); - - // check claimed authorities - assert!(exists_offer(addr), error::not_found(ENOT_OFFERED)); - assert!(has_enough_offer_claimed(addr), error::invalid_state(ENOT_ENOUGH_CLAIMED)); - - // finalize the account - let initial_authorities = get_offer_claimed(addr); - multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); - - // clean offer - clean_offer(addr); - } - - public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { - - let duration_epochs = if (option::is_some(&duration_epochs)) { - *option::borrow(&duration_epochs) - } else { - DEFAULT_EPOCHS_EXPIRE - }; - - Proposal { - proposal_data, - votes: vector::empty
(), - approved: false, - expiration_epoch: epoch_helper::get_current_epoch() + duration_epochs, + + if (multisig_account::is_multisig(multisig_address)) { + // a) finalized account: add authority to the multisig account + let ms = borrow_global_mut(multisig_address); + maybe_update_authorities(ms, true, &vector::singleton(sender_addr)); + if (vector::length(&offer.proposed) == 0) { + // clean the Offer + clean_offer(multisig_address); + }; + } else { + // b) initiated account: add sender to the claimed list + vector::push_back(&mut offer.claimed, sender_addr); + }; + } + + /// Finalizes the multisign account and locks it (cage). + /// - sig: The signer finalizing the account. + /// Aborts if governance is not initialized, the account is already a multisig, + /// there are not enough claimed authorities, or the offer is not found. + public fun finalize_and_cage2(sig: &signer) acquires Offer { + let addr = signer::address_of(sig); + + // check it is not yet initialized + assert!(!multisig_account::is_multisig(addr), error::already_exists(EALREADY_MULTISIG)); + + // check governance + assert!(exists(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + + // check claimed authorities + assert!(exists_offer(addr), error::not_found(ENOT_OFFERED)); + assert!(has_enough_offer_claimed(addr), error::invalid_state(ENOT_ENOUGH_CLAIMED)); + + // finalize the account + let initial_authorities = get_offer_claimed(addr); + multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); + + // clean offer + clean_offer(addr); + } + + public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { + + let duration_epochs = if (option::is_some(&duration_epochs)) { + *option::borrow(&duration_epochs) + } else { + DEFAULT_EPOCHS_EXPIRE + }; + + Proposal { + proposal_data, + votes: vector::empty
(), + approved: false, + expiration_epoch: epoch_helper::get_current_epoch() + duration_epochs, + } + } + + fun assert_authorized(sig: &signer, multisig_address: address) { + // cannot start manipulating contract until the sponsor gave up the auth key + assert_multi_action(multisig_address); + + // check sender is authorized + let sender_addr = signer::address_of(sig); + assert!(is_authority(multisig_address, sender_addr), error::invalid_argument(ENOT_AUTHORIZED)); + } + + // TODO: remove this function after dependencies are updated + public entry fun finalize_and_cage(sig: &signer, initial_authorities: + vector
, num_signers: u64) { + let addr = signer::address_of(sig); + assert!(exists(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + // not yet initialized + assert!(!multisig_account::is_multisig(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + + multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); + } + + //////// Helper functions to check initialization ////////// + #[view] + /// Is the Multisig Governance initialized? + public fun is_multi_action(addr: address): bool { + exists(addr) && + exists>(addr) && + multisig_account::is_multisig(addr) + } + + /// helper to assert if the account is in the right state + fun assert_multi_action(addr: address) { + assert!(multisig_account::is_multisig(addr), error::invalid_argument(ENOT_FINALIZED_NOT_BRICK)); + assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + } + + fun is_gov_init(addr: address): bool { + exists(addr) && + exists>(addr) + } + + // Query if an offer exists for the given multisig address. + public fun exists_offer(multisig_address: address): bool { + exists(multisig_address) + } + + // Query proposed authorities for the given multisig address. + public fun get_offer_proposed(multisig_address: address): vector
acquires Offer { + borrow_global(multisig_address).proposed + } + + // Query claimed authorities for the given multisig address. + public fun get_offer_claimed(multisig_address: address): vector
acquires Offer { + borrow_global(multisig_address).claimed + } + + // Query offer expiration epoch. + public fun get_offer_expiration_epoch(multisig_address: address): vector acquires Offer { + borrow_global(multisig_address).expiration_epoch + } + + // Query if the offer has enough claimed authorities to cage the account. + fun has_enough_offer_claimed(multisig_address: address): bool acquires Offer { + let claimed = get_offer_claimed(multisig_address); + vector::length(&claimed) >= MIN_OFFER_CLAIMS_TO_CAGE } - } - - fun assert_authorized(sig: &signer, multisig_address: address) { - // cannot start manipulating contract until the sponsor gave up the auth key - assert_multi_action(multisig_address); - - // check sender is authorized - let sender_addr = signer::address_of(sig); - assert!(is_authority(multisig_address, sender_addr), error::invalid_argument(ENOT_AUTHORIZED)); - } - - // TODO: remove this function after dependencies are updated - public entry fun finalize_and_cage(sig: &signer, initial_authorities: - vector
, num_signers: u64) { - let addr = signer::address_of(sig); - assert!(exists(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - assert!(exists>(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - // not yet initialized - assert!(!multisig_account::is_multisig(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - - multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); - } - - //////// Helper functions to check initialization ////////// - #[view] - /// Is the Multisig Governance initialized? - public fun is_multi_action(addr: address): bool { - exists(addr) && - exists>(addr) && - multisig_account::is_multisig(addr) - } - - /// helper to assert if the account is in the right state - fun assert_multi_action(addr: address) { - assert!(multisig_account::is_multisig(addr), error::invalid_argument(ENOT_FINALIZED_NOT_BRICK)); - assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); - assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); - } - - fun is_gov_init(addr: address): bool { - exists(addr) && - exists>(addr) - } - - // Query if an offer exists for the given multisig address. - public fun exists_offer(multisig_address: address): bool { - exists(multisig_address) - } - - // Query proposed authorities for the given multisig address. - public fun get_offer_proposed(multisig_address: address): vector
acquires Offer { - borrow_global(multisig_address).proposed - } - - // Query claimed authorities for the given multisig address. - public fun get_offer_claimed(multisig_address: address): vector
acquires Offer { - borrow_global(multisig_address).claimed - } - - // Query offer expiration epoch. - public fun get_offer_expiration_epoch(multisig_address: address): vector acquires Offer { - borrow_global(multisig_address).expiration_epoch - } - - // Query if the offer has enough claimed authorities to cage the account. - fun has_enough_offer_claimed(multisig_address: address): bool acquires Offer { - let claimed = get_offer_claimed(multisig_address); - vector::length(&claimed) >= MIN_OFFER_CLAIMS_TO_CAGE - } - - // Query if the offer has expired. - public fun is_offer_expired(multisig_address: address, authority_address: address): bool acquires Offer { - let offer = borrow_global(multisig_address); - let (_, i) = vector::index_of(&offer.proposed, &authority_address); - let expiration_epoch = vector::borrow(&offer.expiration_epoch, i); - epoch_helper::get_current_epoch() >= *expiration_epoch - } - - /// Has a multisig struct for a given action been created? - public(friend) fun has_action(addr: address):bool { - exists>(addr) - } - - /// An initial "sponsor" who is the signer of the initialization account calls this function. - // This function creates the data structures. - public(friend) fun init_type( - sig: &signer, - can_withdraw: bool, - ) acquires Governance { - let multisig_address = signer::address_of(sig); - // TODO: there is no way of creating a new Action by multisig. The "signer" would need to be spoofed, which account does only in specific and scary situations (e.g. vm_create_account_migration) - - assert!(is_gov_init(multisig_address), error::invalid_argument(EGOV_NOT_INITIALIZED)); - - assert!(!exists>(multisig_address), error::invalid_argument(EACTION_ALREADY_EXISTS)); - // make sure the signer's address is not in the list of authorities. - // This account's signer will now be useless. - // maybe the withdraw cap was never extracted in previous set up. - // but we won't extract it if none of the Actions require it. - if (can_withdraw) { - maybe_extract_withdraw_cap(sig); - }; - - move_to(sig, Action { - can_withdraw, - vote: ballot::new_tracker>(), - }); - } - - fun maybe_extract_withdraw_cap(sig: &signer) acquires Governance { - let multisig_address = signer::address_of(sig); - assert!(exists(multisig_address), error::invalid_argument(ENOT_AUTHORIZED)); - - let ms = borrow_global_mut(multisig_address); - if (option::is_some(&ms.withdraw_capability)) { - return - } else { - let cap = account::extract_withdraw_capability(sig); - option::fill(&mut ms.withdraw_capability, cap); + + // Query if the offer has expired. + public fun is_offer_expired(multisig_address: address, authority_address: address): bool acquires Offer { + let offer = borrow_global(multisig_address); + let (_, i) = vector::index_of(&offer.proposed, &authority_address); + let expiration_epoch = vector::borrow(&offer.expiration_epoch, i); + epoch_helper::get_current_epoch() >= *expiration_epoch + } + + /// Has a multisig struct for a given action been created? + public(friend) fun has_action(addr: address):bool { + exists>(addr) } - } - - /// Withdraw cap is a hot-potato and can never be dropped, we can extract and fill it into a struct that holds it. - - public(friend) fun maybe_restore_withdraw_cap(cap_opt: Option) acquires Governance { - if (option::is_some(&cap_opt)) { - let cap = option::extract(&mut cap_opt); - let addr = account::get_withdraw_cap_address(&cap); - let ms = borrow_global_mut(addr); - option::fill(&mut ms.withdraw_capability, cap); - }; - option::destroy_none(cap_opt); - } - - - // Propose an Action - // Transactions should be easy, and have one obvious way to do it. There should be no other method for voting for a tx. - // this function will catch a duplicate, and vote in its favor. - // This causes a user interface issue, users need to know that you cannot have two open proposals for the same transaction. - // It's optional to state how many epochs from today the transaction should expire. If the transaction is not approved by then, it will be rejected. - // The default will be 14 days. - // Only the first proposer can set the expiration time. It will be ignored when a duplicate is caught. - - public(friend) fun propose_new( - sig: &signer, - multisig_address: address, - proposal_data: Proposal, - ): guid::ID acquires Governance, Action { - assert_authorized(sig, multisig_address); - let ms = borrow_global_mut(multisig_address); - let action = borrow_global_mut>(multisig_address); - // go through all proposals and clean up expired ones. - lazy_cleanup_expired(action); - // does this proposal already exist in the pending list? - let (found, guid, _idx, status_enum, _is_complete) = search_proposals_by_data(&action.vote, &proposal_data); - if (found && status_enum == ballot::get_pending_enum()) { - // this exact proposal is already pending, so we we will just return the guid of the existing proposal. - // we'll let the caller decide what to do (we wont vote by default) - return guid - }; - - let guid = account::create_guid_with_capability(&ms.guid_capability); - let ballot = ballot::propose_ballot(&mut action.vote, guid, proposal_data); - let id = ballot::get_ballot_id(ballot); - - id - } - - - fun vote_with_data(sig: &signer, proposal: &Proposal, multisig_address: address): (bool, Option) acquires Governance, Action { - assert_authorized(sig, multisig_address); - - let action = borrow_global_mut>(multisig_address); - - // does this proposal already exist in the pending list? - let (found, uid, _idx, _status_enum, _is_complete) = search_proposals_by_data(&action.vote, proposal); - - assert!(found, error::invalid_argument(EPROPOSAL_NOT_FOUND)); - - vote_impl(sig, multisig_address, &uid) - - } - - - /// helper function to vote with ID only - public(friend) fun vote_with_id(sig: &signer, id: &guid::ID, multisig_address: address): (bool, Option) acquires Governance, Action { - assert_authorized(sig, multisig_address); - - vote_impl(sig, multisig_address, id) - } - - // TODO: consider using multisig_account also for voting. - // currently only used for governance. - fun vote_impl( - sig: &signer, - multisig_address: address, - id: &guid::ID - ): (bool, Option) acquires Governance, Action { - - assert_authorized(sig, multisig_address); // belt and suspenders - let ms = borrow_global_mut(multisig_address); - let action = borrow_global_mut>(multisig_address); - // always run this to cleanup all missing ballots - lazy_cleanup_expired(action); - - // does this proposal already exist in the pending list? - let (found, _idx, status_enum, is_complete) = ballot::find_anywhere>(&action.vote, id); - assert!(found, error::invalid_argument(EPROPOSAL_NOT_FOUND)); - assert!(status_enum == ballot::get_pending_enum(), error::invalid_state(EVOTING_CLOSED)); - assert!(!is_complete, error::invalid_argument(EVOTING_CLOSED)); - - let b = ballot::get_ballot_by_id_mut(&mut action.vote, id); - - let t = ballot::get_type_struct_mut(b); - let voter_addr = signer::address_of(sig); - // prevent duplicates - assert!(!vector::contains(&t.votes, &voter_addr), - error::invalid_argument(EDUPLICATE_VOTE)); - - vector::push_back(&mut t.votes, voter_addr); - let (n, _m) = get_threshold(multisig_address); - let passed = tally(t, n); - - if (passed) { - ballot::complete_ballot(b); - ballot::move_ballot( - &mut action.vote, - id, - ballot::get_pending_enum(), - ballot::get_approved_enum() - ); - }; - - // get the withdrawal capability, we're not allowed copy, but we can - // extract and fill, and then replace it. See account for an example. - let withdraw_cap = if ( - passed && - option::is_some(&ms.withdraw_capability) && - action.can_withdraw - ) { - let c = option::extract(&mut ms.withdraw_capability); - option::some(c) - } else { - option::none() - }; - - (passed, withdraw_cap) - } - - - // @returns bool, complete and passed - // TODO: Multi_action will never pass a complete and rejected, which needs a UX - fun tally(prop: &mut Proposal, n: u64): bool { - - if (vector::length(&prop.votes) >= n) { - prop.approved = true; - return true - }; - - false - } - - - - fun find_expired(a: & Action): vector{ - let epoch = epoch_helper::get_current_epoch(); - let b_vec = ballot::get_list_ballots_by_enum(&a.vote, ballot::get_pending_enum()); - let id_vec = vector::empty(); - let i = 0; - while (i < vector::length(b_vec)) { - let b = vector::borrow(b_vec, i); - let t = ballot::get_type_struct>(b); - - if (epoch > t.expiration_epoch) { - let id = ballot::get_ballot_id(b); - vector::push_back(&mut id_vec, id); - - }; - i = i + 1; - }; - - id_vec - } - - fun lazy_cleanup_expired(a: &mut Action) { - let expired_vec = find_expired(a); - let len = vector::length(&expired_vec); - let i = 0; - while (i < len) { - let id = vector::borrow(&expired_vec, i); - // lets check the status just in case. - ballot::move_ballot(&mut a.vote, id, ballot::get_pending_enum(), ballot::get_rejected_enum()); - i = i + 1; - }; - } - - fun check_expired(prop: &Proposal): bool { - let epoch_now = epoch_helper::get_current_epoch(); - epoch_now > prop.expiration_epoch - } - - #[view] - public fun is_authority(multisig_addr: address, addr: address): bool { - let auths = multisig_account::owners(multisig_addr); - vector::contains(&auths, &addr) - } - - /// This function is used to copy the data from the proposal that is in the multisig. - /// Note that this is the only way to get the data out of the multisig, and it is the only function to use the `copy` trait. If you have a workflow that needs copying, then the data struct for the action payload will need to use the `copy` trait. - public(friend) fun extract_proposal_data(multisig_address: address, uid: &guid::ID): ProposalData acquires Action { - let a = borrow_global>(multisig_address); - let b = ballot::get_ballot_by_id(&a.vote, uid); - let t = ballot::get_type_struct>(b); - - let Proposal { - proposal_data: existing_data, - expiration_epoch: _, - votes: _, - approved: _, - } = t; - - *existing_data + + /// An initial "sponsor" who is the signer of the initialization account calls this function. + // This function creates the data structures. + public(friend) fun init_type( + sig: &signer, + can_withdraw: bool, + ) acquires Governance { + let multisig_address = signer::address_of(sig); + // TODO: there is no way of creating a new Action by multisig. The "signer" would need to be spoofed, which account does only in specific and scary situations (e.g. vm_create_account_migration) + + assert!(is_gov_init(multisig_address), error::invalid_argument(EGOV_NOT_INITIALIZED)); + + assert!(!exists>(multisig_address), error::invalid_argument(EACTION_ALREADY_EXISTS)); + // make sure the signer's address is not in the list of authorities. + // This account's signer will now be useless. + // maybe the withdraw cap was never extracted in previous set up. + // but we won't extract it if none of the Actions require it. + if (can_withdraw) { + maybe_extract_withdraw_cap(sig); + }; + + move_to(sig, Action { + can_withdraw, + vote: ballot::new_tracker>(), + }); } - /// returns a tuple of (is_found: bool, id: guid:ID, index: u64, status_enum: u8, is_complete: bool) - fun search_proposals_by_data ( - tracker: &BallotTracker>, - data: &Proposal, - ): (bool, guid::ID, u64, u8, bool) { - // looking in pending - - let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_pending_enum()); - if (found) { - let b = ballot::get_ballot_by_id(tracker, &guid); - let complete = ballot::is_completed>(b); - return (true, guid, idx, ballot::get_pending_enum(), complete) - }; - - let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_approved_enum()); - if (found) { - let b = ballot::get_ballot_by_id(tracker, &guid); - let complete = ballot::is_completed>(b); - return (true, guid, idx, ballot::get_approved_enum(), complete) - }; - - let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_rejected_enum()); - if (found) { - let b = ballot::get_ballot_by_id(tracker, &guid); - let complete = ballot::is_completed>(b); - return (true, guid, idx, ballot::get_rejected_enum(), complete) - }; - - (false, guid::create_id(@0x0, 0), 0, 0, false) - } - - /// returns the a tuple with (is_found, id, status_enum ) of ballot while seaching by data - fun find_index_of_ballot_by_data ( - tracker: &BallotTracker>, - incoming_proposal: &Proposal, - status_enum: u8, - ): (bool, guid::ID, u64) { - let Proposal { - proposal_data: incoming_data, - expiration_epoch: _, - votes: _, - approved: _, - } = incoming_proposal; - - let list = ballot::get_list_ballots_by_enum>(tracker, status_enum); - - let i = 0; - while (i < vector::length(list)) { - let b = vector::borrow(list, i); + fun maybe_extract_withdraw_cap(sig: &signer) acquires Governance { + let multisig_address = signer::address_of(sig); + assert!(exists(multisig_address), error::invalid_argument(ENOT_AUTHORIZED)); + + let ms = borrow_global_mut(multisig_address); + if (option::is_some(&ms.withdraw_capability)) { + return + } else { + let cap = account::extract_withdraw_capability(sig); + option::fill(&mut ms.withdraw_capability, cap); + } + } + + /// Withdraw cap is a hot-potato and can never be dropped, we can extract and fill it into a struct that holds it. + + public(friend) fun maybe_restore_withdraw_cap(cap_opt: Option) acquires Governance { + if (option::is_some(&cap_opt)) { + let cap = option::extract(&mut cap_opt); + let addr = account::get_withdraw_cap_address(&cap); + let ms = borrow_global_mut(addr); + option::fill(&mut ms.withdraw_capability, cap); + }; + option::destroy_none(cap_opt); + } + + + // Propose an Action + // Transactions should be easy, and have one obvious way to do it. There should be no other method for voting for a tx. + // this function will catch a duplicate, and vote in its favor. + // This causes a user interface issue, users need to know that you cannot have two open proposals for the same transaction. + // It's optional to state how many epochs from today the transaction should expire. If the transaction is not approved by then, it will be rejected. + // The default will be 14 days. + // Only the first proposer can set the expiration time. It will be ignored when a duplicate is caught. + + public(friend) fun propose_new( + sig: &signer, + multisig_address: address, + proposal_data: Proposal, + ): guid::ID acquires Governance, Action { + assert_authorized(sig, multisig_address); + let ms = borrow_global_mut(multisig_address); + let action = borrow_global_mut>(multisig_address); + // go through all proposals and clean up expired ones. + lazy_cleanup_expired(action); + // does this proposal already exist in the pending list? + let (found, guid, _idx, status_enum, _is_complete) = search_proposals_by_data(&action.vote, &proposal_data); + if (found && status_enum == ballot::get_pending_enum()) { + // this exact proposal is already pending, so we we will just return the guid of the existing proposal. + // we'll let the caller decide what to do (we wont vote by default) + return guid + }; + + let guid = account::create_guid_with_capability(&ms.guid_capability); + let ballot = ballot::propose_ballot(&mut action.vote, guid, proposal_data); + let id = ballot::get_ballot_id(ballot); + + id + } + + + fun vote_with_data(sig: &signer, proposal: &Proposal, multisig_address: address): (bool, Option) acquires Governance, Action { + assert_authorized(sig, multisig_address); + + let action = borrow_global_mut>(multisig_address); + + // does this proposal already exist in the pending list? + let (found, uid, _idx, _status_enum, _is_complete) = search_proposals_by_data(&action.vote, proposal); + + assert!(found, error::invalid_argument(EPROPOSAL_NOT_FOUND)); + + vote_impl(sig, multisig_address, &uid) + + } + + + /// helper function to vote with ID only + public(friend) fun vote_with_id(sig: &signer, id: &guid::ID, multisig_address: address): (bool, Option) acquires Governance, Action { + assert_authorized(sig, multisig_address); + + vote_impl(sig, multisig_address, id) + } + + // TODO: consider using multisig_account also for voting. + // currently only used for governance. + fun vote_impl( + sig: &signer, + multisig_address: address, + id: &guid::ID + ): (bool, Option) acquires Governance, Action { + + assert_authorized(sig, multisig_address); // belt and suspenders + let ms = borrow_global_mut(multisig_address); + let action = borrow_global_mut>(multisig_address); + // always run this to cleanup all missing ballots + lazy_cleanup_expired(action); + + // does this proposal already exist in the pending list? + let (found, _idx, status_enum, is_complete) = ballot::find_anywhere>(&action.vote, id); + assert!(found, error::invalid_argument(EPROPOSAL_NOT_FOUND)); + assert!(status_enum == ballot::get_pending_enum(), error::invalid_state(EVOTING_CLOSED)); + assert!(!is_complete, error::invalid_argument(EVOTING_CLOSED)); + + let b = ballot::get_ballot_by_id_mut(&mut action.vote, id); + + let t = ballot::get_type_struct_mut(b); + let voter_addr = signer::address_of(sig); + // prevent duplicates + assert!(!vector::contains(&t.votes, &voter_addr), + error::invalid_argument(EDUPLICATE_VOTE)); + + vector::push_back(&mut t.votes, voter_addr); + let (n, _m) = get_threshold(multisig_address); + let passed = tally(t, n); + + if (passed) { + ballot::complete_ballot(b); + ballot::move_ballot( + &mut action.vote, + id, + ballot::get_pending_enum(), + ballot::get_approved_enum() + ); + }; + + // get the withdrawal capability, we're not allowed copy, but we can + // extract and fill, and then replace it. See account for an example. + let withdraw_cap = if ( + passed && + option::is_some(&ms.withdraw_capability) && + action.can_withdraw + ) { + let c = option::extract(&mut ms.withdraw_capability); + option::some(c) + } else { + option::none() + }; + + (passed, withdraw_cap) + } + + + // @returns bool, complete and passed + // TODO: Multi_action will never pass a complete and rejected, which needs a UX + fun tally(prop: &mut Proposal, n: u64): bool { + + if (vector::length(&prop.votes) >= n) { + prop.approved = true; + return true + }; + + false + } + + + + fun find_expired(a: & Action): vector{ + let epoch = epoch_helper::get_current_epoch(); + let b_vec = ballot::get_list_ballots_by_enum(&a.vote, ballot::get_pending_enum()); + let id_vec = vector::empty(); + let i = 0; + while (i < vector::length(b_vec)) { + let b = vector::borrow(b_vec, i); + let t = ballot::get_type_struct>(b); + + if (epoch > t.expiration_epoch) { + let id = ballot::get_ballot_id(b); + vector::push_back(&mut id_vec, id); + + }; + i = i + 1; + }; + + id_vec + } + + fun lazy_cleanup_expired(a: &mut Action) { + let expired_vec = find_expired(a); + let len = vector::length(&expired_vec); + let i = 0; + while (i < len) { + let id = vector::borrow(&expired_vec, i); + // lets check the status just in case. + ballot::move_ballot(&mut a.vote, id, ballot::get_pending_enum(), ballot::get_rejected_enum()); + i = i + 1; + }; + } + + fun check_expired(prop: &Proposal): bool { + let epoch_now = epoch_helper::get_current_epoch(); + epoch_now > prop.expiration_epoch + } + + #[view] + public fun is_authority(multisig_addr: address, addr: address): bool { + let auths = multisig_account::owners(multisig_addr); + vector::contains(&auths, &addr) + } + + /// This function is used to copy the data from the proposal that is in the multisig. + /// Note that this is the only way to get the data out of the multisig, and it is the only function to use the `copy` trait. If you have a workflow that needs copying, then the data struct for the action payload will need to use the `copy` trait. + public(friend) fun extract_proposal_data(multisig_address: address, uid: &guid::ID): ProposalData acquires Action { + let a = borrow_global>(multisig_address); + let b = ballot::get_ballot_by_id(&a.vote, uid); let t = ballot::get_type_struct>(b); - // strip the votes and approved fields for comparison let Proposal { proposal_data: existing_data, expiration_epoch: _, @@ -825,214 +763,276 @@ module ol_framework::multi_action { approved: _, } = t; - if (existing_data == incoming_data) { - let uid = ballot::get_ballot_id(b); - return (true, uid, i) + *existing_data + } + + /// returns a tuple of (is_found: bool, id: guid:ID, index: u64, status_enum: u8, is_complete: bool) + fun search_proposals_by_data ( + tracker: &BallotTracker>, + data: &Proposal, + ): (bool, guid::ID, u64, u8, bool) { + // looking in pending + + let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_pending_enum()); + if (found) { + let b = ballot::get_ballot_by_id(tracker, &guid); + let complete = ballot::is_completed>(b); + return (true, guid, idx, ballot::get_pending_enum(), complete) }; - i = i + 1; - }; - (false, guid::create_id(@0x0, 0), 0) + let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_approved_enum()); + if (found) { + let b = ballot::get_ballot_by_id(tracker, &guid); + let complete = ballot::is_completed>(b); + return (true, guid, idx, ballot::get_approved_enum(), complete) + }; + + let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_rejected_enum()); + if (found) { + let b = ballot::get_ballot_by_id(tracker, &guid); + let complete = ballot::is_completed>(b); + return (true, guid, idx, ballot::get_rejected_enum(), complete) + }; + + (false, guid::create_id(@0x0, 0), 0, 0, false) + } + + /// returns the a tuple with (is_found, id, status_enum ) of ballot while seaching by data + fun find_index_of_ballot_by_data ( + tracker: &BallotTracker>, + incoming_proposal: &Proposal, + status_enum: u8, + ): (bool, guid::ID, u64) { + let Proposal { + proposal_data: incoming_data, + expiration_epoch: _, + votes: _, + approved: _, + } = incoming_proposal; + + let list = ballot::get_list_ballots_by_enum>(tracker, status_enum); + + let i = 0; + while (i < vector::length(list)) { + let b = vector::borrow(list, i); + let t = ballot::get_type_struct>(b); + + // strip the votes and approved fields for comparison + let Proposal { + proposal_data: existing_data, + expiration_epoch: _, + votes: _, + approved: _, + } = t; + + if (existing_data == incoming_data) { + let uid = ballot::get_ballot_id(b); + return (true, uid, i) + }; + i = i + 1; + }; + + (false, guid::create_id(@0x0, 0), 0) + } + + /// returns a tuple of (is_found: bool, index: u64, status_enum: u8, is_voting_complete: bool) + public(friend) fun get_proposal_status_by_id(multisig_address: address, uid: &guid::ID): (bool, u64, u8, bool) acquires Action { // found, index, status_enum, is_voting_complete + let a = borrow_global>(multisig_address); + ballot::find_anywhere(&a.vote, uid) + } + + /// get all IDs of multi_auth proposal that are pending + fun get_pending_id(multisig_address: address): vector acquires Action { + let action = borrow_global>(multisig_address); + let list = ballot::get_list_ballots_by_enum(&action.vote, + ballot::get_pending_enum()); + + let id_list = vector::map_ref(list, |el| { + ballot::get_ballot_id(el) + }); + + id_list + } + + + //////// GOVERNANCE //////// + // Governance of the multisig happens through an instance of Action. This action has no special privileges, and is just a normal proposal type. + // The entry point and handler for governance exists on this contract for simplicity. However, there's no reason it couldn't be called from an external contract. + + + /// Tis is a ProposalData type for governance. This Proposal adds or removes a list of addresses as authorities. The handlers are located in this contract. + struct PropGovSigners has key, store, copy, drop { + add_remove: bool, // true = add, false = remove + addresses: vector
, + n_of_m: Option, // Optionally change the n of m threshold. To only change the n_of_m threshold, an empty list of addresses is required. + } + + // TODO: check if the addresses are on chain, does not contain the multisig_address and the current authorities + // Proposing a governance change of adding or removing signer, or changing the n-of-m of the authorities. Note that proposing will deduplicate in the event that two authorities miscommunicate and send the same proposal, in that case for UX purposes the second proposal becomes a vote. + public(friend) fun propose_governance(sig: &signer, multisig_address: address, addresses: vector
, add_remove: bool, n_of_m: Option, duration_epochs: Option ): guid::ID acquires Governance, Action, Offer { + assert_authorized(sig, multisig_address); // Duplicated with propose(), belt + // and suspenders + + validate_owners(&addresses, multisig_address, add_remove); + + let data = PropGovSigners { + addresses, + add_remove, + n_of_m, + }; + + let prop = proposal_constructor(data, duration_epochs); + let id = propose_new(sig, multisig_address, prop); + vote_governance(sig, multisig_address, &id); + + id + } + + /// This function can be called directly. Or the user can call propose_governance() with same parameters, which will deduplicate the proposal and instead vote. Voting is always a positive vote. There is no negative (reject) vote. + public(friend) fun vote_governance(sig: &signer, multisig_address: address, id: &guid::ID): bool acquires Governance, Action, Offer { + assert_authorized(sig, multisig_address); + + let (passed, cap_opt) = { + vote_impl(sig, multisig_address, id) + }; + maybe_restore_withdraw_cap(cap_opt); // don't need this but can't drop. + + if (passed) { + let ms = borrow_global_mut(multisig_address); + let data = extract_proposal_data(multisig_address, id); + if (!vector::is_empty(&data.addresses)) { + if (data.add_remove) { + // offer the authority adition voted to be claimed + add_offer_addresses(multisig_address, data.addresses); + return passed + } else { + maybe_update_authorities(ms, data.add_remove, &data.addresses); + }; + }; + maybe_update_threshold(ms, &data.n_of_m); + }; + passed + } + + /// Updates the authorities of the multisig. This is a helper function for governance. + // must be called with the withdraw capability and signer. belt and suspenders + fun maybe_update_authorities(ms: &mut Governance, add_remove: bool, addresses: &vector
) { + + assert!(!vector::is_empty(addresses), error::invalid_argument(EEMPTY_ADDRESSES)); + + multisig_account::multi_auth_helper_add_remove(&ms.guid_capability, add_remove, addresses); + } + + fun maybe_update_threshold(ms: &mut Governance, n_of_m_opt: &Option) { + if (option::is_some(n_of_m_opt)) { + multisig_account::multi_auth_helper_update_signatures_required(&ms.guid_capability, *option::borrow(n_of_m_opt)); + }; } - /// returns a tuple of (is_found: bool, index: u64, status_enum: u8, is_voting_complete: bool) - public(friend) fun get_proposal_status_by_id(multisig_address: address, uid: &guid::ID): (bool, u64, u8, bool) acquires Action { // found, index, status_enum, is_voting_complete - let a = borrow_global>(multisig_address); - ballot::find_anywhere(&a.vote, uid) - } - - /// get all IDs of multi_auth proposal that are pending - fun get_pending_id(multisig_address: address): vector acquires Action { - let action = borrow_global>(multisig_address); - let list = ballot::get_list_ballots_by_enum(&action.vote, - ballot::get_pending_enum()); - - let id_list = vector::map_ref(list, |el| { - ballot::get_ballot_id(el) - }); - - id_list - } - - - //////// GOVERNANCE //////// - // Governance of the multisig happens through an instance of Action. This action has no special privileges, and is just a normal proposal type. - // The entry point and handler for governance exists on this contract for simplicity. However, there's no reason it couldn't be called from an external contract. - - - /// Tis is a ProposalData type for governance. This Proposal adds or removes a list of addresses as authorities. The handlers are located in this contract. - struct PropGovSigners has key, store, copy, drop { - add_remove: bool, // true = add, false = remove - addresses: vector
, - n_of_m: Option, // Optionally change the n of m threshold. To only change the n_of_m threshold, an empty list of addresses is required. - } - - // TODO: check if the addresses are on chain, does not contain the multisig_address and the current authorities - // Proposing a governance change of adding or removing signer, or changing the n-of-m of the authorities. Note that proposing will deduplicate in the event that two authorities miscommunicate and send the same proposal, in that case for UX purposes the second proposal becomes a vote. - public(friend) fun propose_governance(sig: &signer, multisig_address: address, addresses: vector
, add_remove: bool, n_of_m: Option, duration_epochs: Option ): guid::ID acquires Governance, Action, Offer { - assert_authorized(sig, multisig_address); // Duplicated with propose(), belt - // and suspenders - - validate_owners(&addresses, multisig_address, add_remove); - - let data = PropGovSigners { - addresses, - add_remove, - n_of_m, - }; - - let prop = proposal_constructor(data, duration_epochs); - let id = propose_new(sig, multisig_address, prop); - vote_governance(sig, multisig_address, &id); - - id - } - - /// This function can be called directly. Or the user can call propose_governance() with same parameters, which will deduplicate the proposal and instead vote. Voting is always a positive vote. There is no negative (reject) vote. - public(friend) fun vote_governance(sig: &signer, multisig_address: address, id: &guid::ID): bool acquires Governance, Action, Offer { - assert_authorized(sig, multisig_address); - - let (passed, cap_opt) = { - vote_impl(sig, multisig_address, id) - }; - maybe_restore_withdraw_cap(cap_opt); // don't need this but can't drop. - - if (passed) { - let ms = borrow_global_mut(multisig_address); - let data = extract_proposal_data(multisig_address, id); - if (!vector::is_empty(&data.addresses)) { - if (data.add_remove) { - // offer the authority adition voted to be claimed - add_offer_addresses(multisig_address, data.addresses); - return passed + fun validate_owners(addresses: &vector
, multisig_address: address, add_remove: bool) { + let auths = multisig_account::owners(multisig_address); + let i = 0; + if (add_remove) { + while (i < vector::length(addresses)) { + let addr = vector::borrow(addresses, i); + assert!(!vector::contains(&auths, addr), error::invalid_argument(EALREADY_OWNER)); + i = i + 1; + }; } else { - maybe_update_authorities(ms, data.add_remove, &data.addresses); - }; - }; - maybe_update_threshold(ms, &data.n_of_m); - }; - passed - } - - /// Updates the authorities of the multisig. This is a helper function for governance. - // must be called with the withdraw capability and signer. belt and suspenders - fun maybe_update_authorities(ms: &mut Governance, add_remove: bool, addresses: &vector
) { - - assert!(!vector::is_empty(addresses), error::invalid_argument(EEMPTY_ADDRESSES)); - - multisig_account::multi_auth_helper_add_remove(&ms.guid_capability, add_remove, addresses); - } - - fun maybe_update_threshold(ms: &mut Governance, n_of_m_opt: &Option) { - if (option::is_some(n_of_m_opt)) { - multisig_account::multi_auth_helper_update_signatures_required(&ms.guid_capability, *option::borrow(n_of_m_opt)); - }; - } - - fun validate_owners(addresses: &vector
, multisig_address: address, add_remove: bool) { - let auths = multisig_account::owners(multisig_address); - let i = 0; - if (add_remove) { - while (i < vector::length(addresses)) { - let addr = vector::borrow(addresses, i); - assert!(!vector::contains(&auths, addr), error::invalid_argument(EALREADY_OWNER)); - i = i + 1; - }; - } else { - while (i < vector::length(addresses)) { - let addr = vector::borrow(addresses, i); - assert!(vector::contains(&auths, addr), error::not_found(EOWNER_NOT_FOUND)); - i = i + 1; - }; - }; - multisig_account::validate_owners(addresses, multisig_address); - } - - //////// GETTERS //////// - #[view] - public fun get_authorities(multisig_address: address): vector
{ - multisig_account::owners(multisig_address) - } - - #[view] - public fun get_threshold(multisig_address: address): (u64, u64) { - (multisig_account::num_signatures_required(multisig_address), vector::length(&multisig_account::owners(multisig_address))) - } - - #[view] - /// how many multi_action proposals are pending - public fun get_count_of_pending(multisig_address: address): u64 acquires Action { - let list = get_pending_id(multisig_address); - vector::length(&list) - } - - #[view] - /// the creation number u64 of the pending proposals - public fun get_pending_by_creation_number(multisig_address: address): vector acquires Action { - let list = get_pending_id(multisig_address); - vector::map(list, |el| { - guid::id_creation_num(&el) - }) - } - - - #[view] - /// returns the votes for a given proposal ID. For `view` functions must provide the destructured guid::ID as address and integer. - public fun get_votes(multisig_address: address, id_num: u64): vector
acquires Action { - let action = borrow_global>(multisig_address); - let id = guid::create_id(multisig_address, id_num); - let b = ballot::get_ballot_by_id(&action.vote, &id); - let prop = ballot::get_type_struct(b); - prop.votes - } - - #[view] - /// returns the votes for a given proposal ID. For `view` functions must provide the destructured guid::ID as address and integer. - public fun get_expiration(multisig_address: address, id_num: u64): u64 acquires Action { - let action = borrow_global>(multisig_address); - let id = guid::create_id(multisig_address, id_num); - let b = ballot::get_ballot_by_id(&action.vote, &id); - let prop = ballot::get_type_struct(b); - prop.expiration_epoch - } - - - // TODO: remove this after offer migration is completed - #[test_only] - public(friend) fun init_gov_deprecated(sig: &signer) { - let multisig_address = signer::address_of(sig); - - if (!exists(multisig_address)) { - move_to(sig, Governance { - cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, - cfg_default_n_sigs: 0, // deprecate - signers: vector::empty(), - withdraw_capability: option::none(), - guid_capability: account::create_guid_capability(sig), - }); - }; - - if (!exists>(multisig_address)) { - move_to(sig, Action { - can_withdraw: false, - vote: ballot::new_tracker>(), - }); - }; - } - - // TODO: remove this function after offer migration is completed - #[test_only] - public entry fun finalize_and_cage_deprecated(sig: &signer, initial_authorities: - vector
, num_signers: u64) { - let addr = signer::address_of(sig); - assert!(exists(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - assert!(exists>(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - // not yet initialized - assert!(!multisig_account::is_multisig(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - - multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); - } + while (i < vector::length(addresses)) { + let addr = vector::borrow(addresses, i); + assert!(vector::contains(&auths, addr), error::not_found(EOWNER_NOT_FOUND)); + i = i + 1; + }; + }; + multisig_account::validate_owners(addresses, multisig_address); + } + + //////// GETTERS //////// + #[view] + public fun get_authorities(multisig_address: address): vector
{ + multisig_account::owners(multisig_address) + } + + #[view] + public fun get_threshold(multisig_address: address): (u64, u64) { + (multisig_account::num_signatures_required(multisig_address), vector::length(&multisig_account::owners(multisig_address))) + } + + #[view] + /// how many multi_action proposals are pending + public fun get_count_of_pending(multisig_address: address): u64 acquires Action { + let list = get_pending_id(multisig_address); + vector::length(&list) + } + + #[view] + /// the creation number u64 of the pending proposals + public fun get_pending_by_creation_number(multisig_address: address): vector acquires Action { + let list = get_pending_id(multisig_address); + vector::map(list, |el| { + guid::id_creation_num(&el) + }) + } + + + #[view] + /// returns the votes for a given proposal ID. For `view` functions must provide the destructured guid::ID as address and integer. + public fun get_votes(multisig_address: address, id_num: u64): vector
acquires Action { + let action = borrow_global>(multisig_address); + let id = guid::create_id(multisig_address, id_num); + let b = ballot::get_ballot_by_id(&action.vote, &id); + let prop = ballot::get_type_struct(b); + prop.votes + } + + #[view] + /// returns the votes for a given proposal ID. For `view` functions must provide the destructured guid::ID as address and integer. + public fun get_expiration(multisig_address: address, id_num: u64): u64 acquires Action { + let action = borrow_global>(multisig_address); + let id = guid::create_id(multisig_address, id_num); + let b = ballot::get_ballot_by_id(&action.vote, &id); + let prop = ballot::get_type_struct(b); + prop.expiration_epoch + } + + + // TODO: remove this after offer migration is completed + #[test_only] + public(friend) fun init_gov_deprecated(sig: &signer) { + let multisig_address = signer::address_of(sig); + + if (!exists(multisig_address)) { + move_to(sig, Governance { + cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, + cfg_default_n_sigs: 0, // deprecate + signers: vector::empty(), + withdraw_capability: option::none(), + guid_capability: account::create_guid_capability(sig), + }); + }; + + if (!exists>(multisig_address)) { + move_to(sig, Action { + can_withdraw: false, + vote: ballot::new_tracker>(), + }); + }; + } + + // TODO: remove this function after offer migration is completed + #[test_only] + public entry fun finalize_and_cage_deprecated(sig: &signer, initial_authorities: + vector
, num_signers: u64) { + let addr = signer::address_of(sig); + assert!(exists(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + // not yet initialized + assert!(!multisig_account::is_multisig(addr), + error::invalid_argument(EGOV_NOT_INITIALIZED)); + + multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); + } } From a26f75386686661fe52d961885090a9848d2ca45 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 17 Jun 2024 19:54:30 -0300 Subject: [PATCH 26/68] creates module multi_action_migration - WIP --- .../sources/create_signer.move | 2 +- .../sources/multisig_account.move | 1 + .../tests/vote_lib/multi_action.test.move | 100 ----- .../vote_lib/multi_action_migration.test.move | 110 ++++++ .../ol_sources/vote_lib/multi_action.move | 345 ++++++++---------- .../vote_lib/multi_action_migration.move | 48 +++ 6 files changed, 319 insertions(+), 287 deletions(-) create mode 100644 framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action_migration.test.move create mode 100644 framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move diff --git a/framework/libra-framework/sources/create_signer.move b/framework/libra-framework/sources/create_signer.move index 8ea011724..a64c29882 100644 --- a/framework/libra-framework/sources/create_signer.move +++ b/framework/libra-framework/sources/create_signer.move @@ -19,7 +19,7 @@ module diem_framework::create_signer { //////// 0L //////// friend ol_framework::fee_maker; friend ol_framework::epoch_boundary; - friend ol_framework::multi_action; + friend ol_framework::multi_action_migration; // TODO: remove after offer migration is completed public(friend) native fun create_signer(addr: address): signer; } diff --git a/framework/libra-framework/sources/multisig_account.move b/framework/libra-framework/sources/multisig_account.move index 5f9a19645..b29127d47 100644 --- a/framework/libra-framework/sources/multisig_account.move +++ b/framework/libra-framework/sources/multisig_account.move @@ -56,6 +56,7 @@ module diem_framework::multisig_account { // use diem_std::debug::print; friend ol_framework::multi_action; + friend ol_framework::multi_action_migration; /// The salt used to create a resource account during multisig account creation. /// This is used to avoid conflicts with other modules that also create resource accounts with the same owner diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index dc6d594c8..1033f598e 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -587,106 +587,6 @@ module ol_framework::test_multi_action { multi_action::finalize_and_cage2(alice); } - // Offer Migration tests - - // Happy Day: init offer on a legacy multisign initiated account - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - fun migrate_offer_account_initialized(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let alice_address = signer::address_of(alice); - - // initialize the multi_action account - multi_action::init_gov_deprecated(alice); - assert!(multi_action::exists_offer(alice_address) == false, 7357001); - - // migrate the offer - multi_action::migrate_offer(alice, alice_address); - assert!(multi_action::exists_offer(alice_address) == true, 7357002); - - // offer authority to bob - multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(9)); - - // check the offer is proposed - assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000b), 7357003); - assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357004); - assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(9), 7357005); - } - - // Happy Day: init offer on a legacy mulstisign finalized account - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - fun migrate_offer_account_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { - let _vals = mock::genesis_n_vals(root, 4); - let alice_address = signer::address_of(alice); - - // make alice account multisign with deprecated methods - multi_action::init_gov_deprecated(alice); - let authorities = vector::singleton(@0x1000b); - vector::push_back(&mut authorities, @0x1000c); - multi_action::finalize_and_cage_deprecated(alice, authorities, 2); - assert!(multi_action::exists_offer(alice_address) == false, 7357001); - - // carol migrate the offer - multi_action::migrate_offer(carol, alice_address); - assert!(multi_action::exists_offer(alice_address) == true, 7357002); - - // carol propose dave as new authority - let id = multi_action::propose_governance(carol, alice_address, - vector::singleton(@0x1000d), true, option::none(), - option::none()); - - // bob votes - let passed = multi_action::vote_governance(bob, alice_address, &id); - assert!(passed, 7357003); - - // dave claim the offer - multi_action::claim_offer(dave, alice_address); - - // check new authorities - let authorities = vector::singleton(@0x1000b); - vector::push_back(&mut authorities, @0x1000c); - vector::push_back(&mut authorities, @0x1000d); - assert!(multi_action::get_authorities(alice_address) == authorities, 7357004); - } - - // Try to migrate offer of an account already with offer - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x80025, location = ol_framework::multi_action)] - fun migrate_offer_account_with_offer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let alice_address = signer::address_of(alice); - - // initialize the multi_action account - multi_action::init_gov(alice); - - // try to migrate offer - multi_action::migrate_offer(alice, alice_address); - } - - // Try to migrate offer of an account without governance initiated - #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] - fun migrate_offer_account_without_gov(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); - let alice_address = signer::address_of(alice); - - // try to migrate offer - multi_action::migrate_offer(alice, alice_address); - } - - // Try to migrate offer through someone without authority - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x50003, location = ol_framework::multi_action)] - fun migrate_offer_without_authority(root: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let alice_address = signer::address_of(alice); - - // initialize the multi_action account - multi_action::init_gov_deprecated(alice); - - // try to migrate offer - multi_action::migrate_offer(bob, alice_address); - } - // Governance Tests // Happy Day: propose a new action and check zero votes diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action_migration.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action_migration.test.move new file mode 100644 index 000000000..9c03db6cd --- /dev/null +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action_migration.test.move @@ -0,0 +1,110 @@ +#[test_only] +module ol_framework::test_multi_action_migration { + use ol_framework::mock; + use ol_framework::multi_action; + use ol_framework::multi_action_migration; + use std::signer; + use std::option; + use std::vector; + + // print + // use std::debug::print; + + // Happy Day: init offer on a legacy multisign initiated account + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + fun migrate_offer_account_initialized(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov_deprecated(alice); + assert!(multi_action::exists_offer(alice_address) == false, 7357001); + + // migrate the offer + multi_action_migration::migrate_offer(alice, alice_address); + assert!(multi_action::exists_offer(alice_address) == true, 7357002); + + // offer authority to bob + multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(9)); + + // check the offer is proposed + assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000b), 7357003); + assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357004); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(9), 7357005); + } + + // Happy Day: init offer on a legacy mulstisign finalized account + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun migrate_offer_account_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + let _vals = mock::genesis_n_vals(root, 4); + let alice_address = signer::address_of(alice); + + // make alice account multisign with deprecated methods + multi_action::init_gov_deprecated(alice); + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + multi_action::finalize_and_cage_deprecated(alice, authorities, 2); + assert!(multi_action::exists_offer(alice_address) == false, 7357001); + + // carol migrate the offer + multi_action_migration::migrate_offer(carol, alice_address); + assert!(multi_action::exists_offer(alice_address) == true, 7357002); + + // carol propose dave as new authority + let id = multi_action::propose_governance(carol, alice_address, + vector::singleton(@0x1000d), true, option::none(), + option::none()); + + // bob votes + let passed = multi_action::vote_governance(bob, alice_address, &id); + assert!(passed, 7357003); + + // dave claim the offer + multi_action::claim_offer(dave, alice_address); + + // check new authorities + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + assert!(multi_action::get_authorities(alice_address) == authorities, 7357004); + } + + // Try to migrate offer of an account already with offer + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x80001, location = ol_framework::multi_action_migration)] + fun migrate_offer_account_with_offer(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov(alice); + + // try to migrate offer + multi_action_migration::migrate_offer(alice, alice_address); + } + + // Try to migrate offer of an account without governance initiated + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x30003, location = ol_framework::multi_action_migration)] + fun migrate_offer_account_without_gov(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + let alice_address = signer::address_of(alice); + + // try to migrate offer + multi_action_migration::migrate_offer(alice, alice_address); + } + + // Try to migrate offer through someone without authority + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x50002, location = ol_framework::multi_action_migration)] + fun migrate_offer_without_authority(root: &signer, alice: &signer, bob: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov_deprecated(alice); + + // try to migrate offer + multi_action_migration::migrate_offer(bob, alice_address); + } +} \ No newline at end of file diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 1f86ff7b3..fbf297386 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -28,7 +28,6 @@ module ol_framework::multi_action { use std::signer; use std::error; use std::guid; - use diem_framework::create_signer::create_signer; use diem_framework::account::{Self, WithdrawCapability}; use diem_framework::multisig_account; use ol_framework::ballot::{Self, BallotTracker}; @@ -40,6 +39,12 @@ module ol_framework::multi_action { friend ol_framework::donor_voice_txs; friend ol_framework::safe; + // TODO: Remove after migration + friend ol_framework::multi_action_migration; + + #[test_only] + friend ol_framework::test_multi_action_migration; // TODO: remove after offer migration + #[test_only] friend ol_framework::test_multi_action; @@ -165,9 +170,9 @@ module ol_framework::multi_action { fun construct_empty_offer(): Offer { Offer { - proposed: vector::empty(), - claimed: vector::empty(), - expiration_epoch: vector::empty(), + proposed: vector::empty(), + claimed: vector::empty(), + expiration_epoch: vector::empty(), } } @@ -178,6 +183,12 @@ module ol_framework::multi_action { offer.expiration_epoch = vector::empty(); } + public(friend) fun init_offer(sig: &signer, addr: address) { + if (!exists(addr)) { + move_to(sig, construct_empty_offer()); + }; + } + // Initialize the governance structs for this account. // Governance contains the constraints for each Action that are checked on each vote (n_sigs, expiration, signers, etc) // Also, an initial Action of type PropGovSigners is created, which is used to govern the signers and threshold for this account. @@ -189,24 +200,22 @@ module ol_framework::multi_action { if (!exists(multisig_address)) { move_to(sig, Governance { - cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, - cfg_default_n_sigs: 0, // deprecate - signers: vector::empty(), - withdraw_capability: option::none(), - guid_capability: account::create_guid_capability(sig), - }); + cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, + cfg_default_n_sigs: 0, // deprecate + signers: vector::empty(), + withdraw_capability: option::none(), + guid_capability: account::create_guid_capability(sig), + }); }; if (!exists>(multisig_address)) { - move_to(sig, Action { - can_withdraw: false, - vote: ballot::new_tracker>(), - }); + move_to(sig, Action { + can_withdraw: false, + vote: ballot::new_tracker>(), + }); }; - if (!exists(multisig_address)) { - move_to(sig, construct_empty_offer()); - }; + init_offer(sig, multisig_address); } // Private function to assist governance vote @@ -222,36 +231,6 @@ module ol_framework::multi_action { }; } - // DANGER - may forge the signer of the multisig account is necessary here - // TODO: remove this function after offer migration is completed - // Migrate a legacy account to have structure Offer in order to propose authorities changes - public entry fun migrate_offer(sig: &signer, multisig_address: address) { - // Ensure the account does not have Offer structure - assert!(!exists_offer(multisig_address), error::already_exists(EOFFER_ALREADY_EXISTS)); - - // if account is multisig, forge signer and add Offer to the multisig account - if (multisig_account::is_multisig(multisig_address)) { - // a) multisig account: ensure the signer is in the authorities list - assert!(is_authority(multisig_address, signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); - - // We create the signer for the multisig account here since this is required - // to add the Offer resource. - // This should be safe because we check that the signer is in the authorities list. - // Also, after all accounts are migrated this function will be deprecated. - let multisig_signer = &create_signer(multisig_address); // <<< DANGER - - // create Offer structure - let offer = construct_empty_offer(); - move_to(multisig_signer, offer); - } else { - // b) initiated account: ensure the account is initialized with governance and add Offer to the account - assert!(multisig_address == signer::address_of(sig), error::permission_denied(ENOT_AUTHORIZED)); - assert!(is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); - let offer = construct_empty_offer(); - move_to(sig, offer); - }; - } - fun ensure_valid_propose_offer_state(addr: address) { // Ensure the account is not yet initialized as multisig assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); @@ -262,7 +241,6 @@ module ol_framework::multi_action { } fun ensure_valid_propose_offer_params(addr: address, proposed: vector
, duration_epochs: Option) { - // Ensure the proposed list is not empty assert!(vector::length(&proposed) > 0, error::invalid_argument(EOFFER_EMPTY)); @@ -282,9 +260,9 @@ module ol_framework::multi_action { // Calculate the expiration epoch for the offer. fun calculate_expiration_epoch(duration_epochs: Option): u64 { let duration_epochs = if (option::is_some(&duration_epochs)) { - *option::borrow(&duration_epochs) + *option::borrow(&duration_epochs) } else { - DEFAULT_EPOCHS_OFFER_EXPIRE + DEFAULT_EPOCHS_OFFER_EXPIRE }; epoch_helper::get_current_epoch() + duration_epochs @@ -300,50 +278,50 @@ module ol_framework::multi_action { // Remove claimed addresses that are not in the new proposed list let j = 0; while (j < vector::length(&offer.claimed)) { - let claimed_addr = vector::borrow(&offer.claimed, j); - if (!vector::contains(&proposed, claimed_addr)) { - vector::remove(&mut offer.claimed, j); - } else { - j = j + 1; - }; + let claimed_addr = vector::borrow(&offer.claimed, j); + if (!vector::contains(&proposed, claimed_addr)) { + vector::remove(&mut offer.claimed, j); + } else { + j = j + 1; + }; }; // Remove new proposed addresses that are already claimed let i = 0; while (i < vector::length(&proposed)) { - let proposed_addr = vector::borrow(&proposed, i); - if (vector::contains(&offer.claimed, proposed_addr)) { - vector::remove(&mut proposed, i); - }; - i = i + 1; + let proposed_addr = vector::borrow(&proposed, i); + if (vector::contains(&offer.claimed, proposed_addr)) { + vector::remove(&mut proposed, i); + }; + i = i + 1; }; // Remove old proposed addresses that are not in the new proposed list let j = 0; while (j < vector::length(&offer.proposed)) { - let proposed_addr = vector::borrow(&offer.proposed, j); - if (!vector::contains(&proposed, proposed_addr)) { - vector::remove(&mut offer.proposed, j); - vector::remove(&mut offer.expiration_epoch, j); - } else { - j = j + 1; - }; + let proposed_addr = vector::borrow(&offer.proposed, j); + if (!vector::contains(&proposed, proposed_addr)) { + vector::remove(&mut offer.proposed, j); + vector::remove(&mut offer.expiration_epoch, j); + } else { + j = j + 1; + }; }; // Insert/Update proposed and expiration epoch lists let k = 0; while (k < vector::length(&proposed)) { - // if already contains the address, update the expiration_epoch - let proposed_addr = vector::borrow(&proposed, k); - let (found, i) = vector::index_of(&offer.proposed, proposed_addr); - if (found) { - vector::remove(&mut offer.expiration_epoch, i); - vector::insert(&mut offer.expiration_epoch, i, expiration_epoch); - } else { - vector::push_back(&mut offer.proposed, *proposed_addr); - vector::push_back(&mut offer.expiration_epoch, expiration_epoch); - }; - k = k + 1; + // if already contains the address, update the expiration_epoch + let proposed_addr = vector::borrow(&proposed, k); + let (found, i) = vector::index_of(&offer.proposed, proposed_addr); + if (found) { + vector::remove(&mut offer.expiration_epoch, i); + vector::insert(&mut offer.expiration_epoch, i, expiration_epoch); + } else { + vector::push_back(&mut offer.proposed, *proposed_addr); + vector::push_back(&mut offer.expiration_epoch, expiration_epoch); + }; + k = k + 1; }; } @@ -386,16 +364,16 @@ module ol_framework::multi_action { vector::remove(&mut offer.expiration_epoch, i); if (multisig_account::is_multisig(multisig_address)) { - // a) finalized account: add authority to the multisig account - let ms = borrow_global_mut(multisig_address); - maybe_update_authorities(ms, true, &vector::singleton(sender_addr)); - if (vector::length(&offer.proposed) == 0) { - // clean the Offer - clean_offer(multisig_address); - }; + // a) finalized account: add authority to the multisig account + let ms = borrow_global_mut(multisig_address); + maybe_update_authorities(ms, true, &vector::singleton(sender_addr)); + if (vector::length(&offer.proposed) == 0) { + // clean the Offer + clean_offer(multisig_address); + }; } else { - // b) initiated account: add sender to the claimed list - vector::push_back(&mut offer.claimed, sender_addr); + // b) initiated account: add sender to the claimed list + vector::push_back(&mut offer.claimed, sender_addr); }; } @@ -428,16 +406,16 @@ module ol_framework::multi_action { public(friend) fun proposal_constructor(proposal_data: ProposalData, duration_epochs: Option): Proposal { let duration_epochs = if (option::is_some(&duration_epochs)) { - *option::borrow(&duration_epochs) + *option::borrow(&duration_epochs) } else { - DEFAULT_EPOCHS_EXPIRE + DEFAULT_EPOCHS_EXPIRE }; Proposal { - proposal_data, - votes: vector::empty
(), - approved: false, - expiration_epoch: epoch_helper::get_current_epoch() + duration_epochs, + proposal_data, + votes: vector::empty
(), + approved: false, + expiration_epoch: epoch_helper::get_current_epoch() + duration_epochs, } } @@ -451,8 +429,7 @@ module ol_framework::multi_action { } // TODO: remove this function after dependencies are updated - public entry fun finalize_and_cage(sig: &signer, initial_authorities: - vector
, num_signers: u64) { + public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { let addr = signer::address_of(sig); assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); @@ -481,7 +458,8 @@ module ol_framework::multi_action { assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); } - fun is_gov_init(addr: address): bool { + // Check if the account is a multisig and has the Governance initialized + public fun is_gov_init(addr: address): bool { exists(addr) && exists>(addr) } @@ -542,7 +520,7 @@ module ol_framework::multi_action { // maybe the withdraw cap was never extracted in previous set up. // but we won't extract it if none of the Actions require it. if (can_withdraw) { - maybe_extract_withdraw_cap(sig); + maybe_extract_withdraw_cap(sig); }; move_to(sig, Action { @@ -557,10 +535,10 @@ module ol_framework::multi_action { let ms = borrow_global_mut(multisig_address); if (option::is_some(&ms.withdraw_capability)) { - return + return } else { - let cap = account::extract_withdraw_capability(sig); - option::fill(&mut ms.withdraw_capability, cap); + let cap = account::extract_withdraw_capability(sig); + option::fill(&mut ms.withdraw_capability, cap); } } @@ -568,10 +546,10 @@ module ol_framework::multi_action { public(friend) fun maybe_restore_withdraw_cap(cap_opt: Option) acquires Governance { if (option::is_some(&cap_opt)) { - let cap = option::extract(&mut cap_opt); - let addr = account::get_withdraw_cap_address(&cap); - let ms = borrow_global_mut(addr); - option::fill(&mut ms.withdraw_capability, cap); + let cap = option::extract(&mut cap_opt); + let addr = account::get_withdraw_cap_address(&cap); + let ms = borrow_global_mut(addr); + option::fill(&mut ms.withdraw_capability, cap); }; option::destroy_none(cap_opt); } @@ -598,9 +576,9 @@ module ol_framework::multi_action { // does this proposal already exist in the pending list? let (found, guid, _idx, status_enum, _is_complete) = search_proposals_by_data(&action.vote, &proposal_data); if (found && status_enum == ballot::get_pending_enum()) { - // this exact proposal is already pending, so we we will just return the guid of the existing proposal. - // we'll let the caller decide what to do (we wont vote by default) - return guid + // this exact proposal is already pending, so we we will just return the guid of the existing proposal. + // we'll let the caller decide what to do (we wont vote by default) + return guid }; let guid = account::create_guid_with_capability(&ms.guid_capability); @@ -666,26 +644,26 @@ module ol_framework::multi_action { let passed = tally(t, n); if (passed) { - ballot::complete_ballot(b); - ballot::move_ballot( - &mut action.vote, - id, - ballot::get_pending_enum(), - ballot::get_approved_enum() - ); + ballot::complete_ballot(b); + ballot::move_ballot( + &mut action.vote, + id, + ballot::get_pending_enum(), + ballot::get_approved_enum() + ); }; // get the withdrawal capability, we're not allowed copy, but we can // extract and fill, and then replace it. See account for an example. let withdraw_cap = if ( - passed && - option::is_some(&ms.withdraw_capability) && - action.can_withdraw + passed && + option::is_some(&ms.withdraw_capability) && + action.can_withdraw ) { - let c = option::extract(&mut ms.withdraw_capability); - option::some(c) + let c = option::extract(&mut ms.withdraw_capability); + option::some(c) } else { - option::none() + option::none() }; (passed, withdraw_cap) @@ -695,12 +673,10 @@ module ol_framework::multi_action { // @returns bool, complete and passed // TODO: Multi_action will never pass a complete and rejected, which needs a UX fun tally(prop: &mut Proposal, n: u64): bool { - if (vector::length(&prop.votes) >= n) { - prop.approved = true; - return true + prop.approved = true; + return true }; - false } @@ -712,15 +688,15 @@ module ol_framework::multi_action { let id_vec = vector::empty(); let i = 0; while (i < vector::length(b_vec)) { - let b = vector::borrow(b_vec, i); - let t = ballot::get_type_struct>(b); + let b = vector::borrow(b_vec, i); + let t = ballot::get_type_struct>(b); - if (epoch > t.expiration_epoch) { - let id = ballot::get_ballot_id(b); - vector::push_back(&mut id_vec, id); + if (epoch > t.expiration_epoch) { + let id = ballot::get_ballot_id(b); + vector::push_back(&mut id_vec, id); - }; - i = i + 1; + }; + i = i + 1; }; id_vec @@ -731,10 +707,10 @@ module ol_framework::multi_action { let len = vector::length(&expired_vec); let i = 0; while (i < len) { - let id = vector::borrow(&expired_vec, i); - // lets check the status just in case. - ballot::move_ballot(&mut a.vote, id, ballot::get_pending_enum(), ballot::get_rejected_enum()); - i = i + 1; + let id = vector::borrow(&expired_vec, i); + // lets check the status just in case. + ballot::move_ballot(&mut a.vote, id, ballot::get_pending_enum(), ballot::get_rejected_enum()); + i = i + 1; }; } @@ -764,45 +740,45 @@ module ol_framework::multi_action { } = t; *existing_data - } + } - /// returns a tuple of (is_found: bool, id: guid:ID, index: u64, status_enum: u8, is_complete: bool) - fun search_proposals_by_data ( + /// returns a tuple of (is_found: bool, id: guid:ID, index: u64, status_enum: u8, is_complete: bool) + fun search_proposals_by_data ( tracker: &BallotTracker>, data: &Proposal, - ): (bool, guid::ID, u64, u8, bool) { + ): (bool, guid::ID, u64, u8, bool) { // looking in pending let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_pending_enum()); if (found) { - let b = ballot::get_ballot_by_id(tracker, &guid); - let complete = ballot::is_completed>(b); - return (true, guid, idx, ballot::get_pending_enum(), complete) + let b = ballot::get_ballot_by_id(tracker, &guid); + let complete = ballot::is_completed>(b); + return (true, guid, idx, ballot::get_pending_enum(), complete) }; let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_approved_enum()); if (found) { - let b = ballot::get_ballot_by_id(tracker, &guid); - let complete = ballot::is_completed>(b); - return (true, guid, idx, ballot::get_approved_enum(), complete) + let b = ballot::get_ballot_by_id(tracker, &guid); + let complete = ballot::is_completed>(b); + return (true, guid, idx, ballot::get_approved_enum(), complete) }; let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_rejected_enum()); - if (found) { - let b = ballot::get_ballot_by_id(tracker, &guid); - let complete = ballot::is_completed>(b); - return (true, guid, idx, ballot::get_rejected_enum(), complete) + if (found) { + let b = ballot::get_ballot_by_id(tracker, &guid); + let complete = ballot::is_completed>(b); + return (true, guid, idx, ballot::get_rejected_enum(), complete) }; (false, guid::create_id(@0x0, 0), 0, 0, false) } - /// returns the a tuple with (is_found, id, status_enum ) of ballot while seaching by data - fun find_index_of_ballot_by_data ( + /// returns the a tuple with (is_found, id, status_enum ) of ballot while seaching by data + fun find_index_of_ballot_by_data ( tracker: &BallotTracker>, incoming_proposal: &Proposal, status_enum: u8, - ): (bool, guid::ID, u64) { + ): (bool, guid::ID, u64) { let Proposal { proposal_data: incoming_data, expiration_epoch: _, @@ -833,7 +809,7 @@ module ol_framework::multi_action { }; (false, guid::create_id(@0x0, 0), 0) - } + } /// returns a tuple of (is_found: bool, index: u64, status_enum: u8, is_voting_complete: bool) public(friend) fun get_proposal_status_by_id(multisig_address: address, uid: &guid::ID): (bool, u64, u8, bool) acquires Action { // found, index, status_enum, is_voting_complete @@ -848,7 +824,7 @@ module ol_framework::multi_action { ballot::get_pending_enum()); let id_list = vector::map_ref(list, |el| { - ballot::get_ballot_id(el) + ballot::get_ballot_id(el) }); id_list @@ -876,9 +852,9 @@ module ol_framework::multi_action { validate_owners(&addresses, multisig_address, add_remove); let data = PropGovSigners { - addresses, - add_remove, - n_of_m, + addresses, + add_remove, + n_of_m, }; let prop = proposal_constructor(data, duration_epochs); @@ -893,23 +869,23 @@ module ol_framework::multi_action { assert_authorized(sig, multisig_address); let (passed, cap_opt) = { - vote_impl(sig, multisig_address, id) + vote_impl(sig, multisig_address, id) }; maybe_restore_withdraw_cap(cap_opt); // don't need this but can't drop. if (passed) { - let ms = borrow_global_mut(multisig_address); - let data = extract_proposal_data(multisig_address, id); - if (!vector::is_empty(&data.addresses)) { - if (data.add_remove) { - // offer the authority adition voted to be claimed - add_offer_addresses(multisig_address, data.addresses); - return passed - } else { - maybe_update_authorities(ms, data.add_remove, &data.addresses); + let ms = borrow_global_mut(multisig_address); + let data = extract_proposal_data(multisig_address, id); + if (!vector::is_empty(&data.addresses)) { + if (data.add_remove) { + // offer the authority adition voted to be claimed + add_offer_addresses(multisig_address, data.addresses); + return passed + } else { + maybe_update_authorities(ms, data.add_remove, &data.addresses); + }; }; - }; - maybe_update_threshold(ms, &data.n_of_m); + maybe_update_threshold(ms, &data.n_of_m); }; passed } @@ -917,9 +893,7 @@ module ol_framework::multi_action { /// Updates the authorities of the multisig. This is a helper function for governance. // must be called with the withdraw capability and signer. belt and suspenders fun maybe_update_authorities(ms: &mut Governance, add_remove: bool, addresses: &vector
) { - assert!(!vector::is_empty(addresses), error::invalid_argument(EEMPTY_ADDRESSES)); - multisig_account::multi_auth_helper_add_remove(&ms.guid_capability, add_remove, addresses); } @@ -956,7 +930,7 @@ module ol_framework::multi_action { #[view] public fun get_threshold(multisig_address: address): (u64, u64) { - (multisig_account::num_signatures_required(multisig_address), vector::length(&multisig_account::owners(multisig_address))) + (multisig_account::num_signatures_required(multisig_address), vector::length(&multisig_account::owners(multisig_address))) } #[view] @@ -971,7 +945,7 @@ module ol_framework::multi_action { public fun get_pending_by_creation_number(multisig_address: address): vector acquires Action { let list = get_pending_id(multisig_address); vector::map(list, |el| { - guid::id_creation_num(&el) + guid::id_creation_num(&el) }) } @@ -1004,26 +978,25 @@ module ol_framework::multi_action { if (!exists(multisig_address)) { move_to(sig, Governance { - cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, - cfg_default_n_sigs: 0, // deprecate - signers: vector::empty(), - withdraw_capability: option::none(), - guid_capability: account::create_guid_capability(sig), - }); + cfg_duration_epochs: DEFAULT_EPOCHS_EXPIRE, + cfg_default_n_sigs: 0, // deprecate + signers: vector::empty(), + withdraw_capability: option::none(), + guid_capability: account::create_guid_capability(sig), + }); }; if (!exists>(multisig_address)) { - move_to(sig, Action { - can_withdraw: false, - vote: ballot::new_tracker>(), - }); + move_to(sig, Action { + can_withdraw: false, + vote: ballot::new_tracker>(), + }); }; } // TODO: remove this function after offer migration is completed #[test_only] - public entry fun finalize_and_cage_deprecated(sig: &signer, initial_authorities: - vector
, num_signers: u64) { + public entry fun finalize_and_cage_deprecated(sig: &signer, initial_authorities: vector
, num_signers: u64) { let addr = signer::address_of(sig); assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move new file mode 100644 index 000000000..2a6ea317b --- /dev/null +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move @@ -0,0 +1,48 @@ +module ol_framework::multi_action_migration { + use std::signer; + use std::error; + use diem_framework::create_signer::create_signer; + use diem_framework::multisig_account; + use ol_framework::multi_action; + + #[test_only] + friend ol_framework::test_multi_action; + + //#[text_only] + //friend ol_framework::test_multi_action_migration; + + /// Account already has Offer structure + const EOFFER_ALREADY_EXISTS: u64 = 0x1; + /// Signer is not in the authorities list + const ENOT_AUTHORIZED: u64 = 0x2; + /// Governance is not initialized + const EGOV_NOT_INITIALIZED: u64 = 0x3; + + // DANGER - may forge the signer of the multisig account is necessary here + // TODO: remove this function after offer migration is completed + // Migrate a legacy account to have structure Offer in order to propose authorities changes + public entry fun migrate_offer(sig: &signer, multisig_address: address) { + // Ensure the account does not have Offer structure + assert!(!multi_action::exists_offer(multisig_address), error::already_exists(EOFFER_ALREADY_EXISTS)); + + // if account is multisig, forge signer and add Offer to the multisig account + if (multisig_account::is_multisig(multisig_address)) { + // a) multisig account: ensure the signer is in the authorities list + assert!(multi_action::is_authority(multisig_address, signer::address_of(sig)), error::permission_denied(ENOT_AUTHORIZED)); + + // We create the signer for the multisig account here since this is required + // to add the Offer resource. + // This should be safe because we check that the signer is in the authorities list. + // Also, after all accounts are migrated this function will be deprecated. + let multisig_signer = &create_signer(multisig_address); // <<< DANGER + + // create Offer structure + multi_action::init_offer(multisig_signer, multisig_address); + } else { + // b) initiated account: ensure the account is initialized with governance and add Offer to the account + assert!(multisig_address == signer::address_of(sig), error::permission_denied(ENOT_AUTHORIZED)); + assert!(multi_action::is_gov_init(multisig_address), error::invalid_state(EGOV_NOT_INITIALIZED)); + multi_action::init_offer(sig, multisig_address); + }; + } +} \ No newline at end of file From 7d3ddd8cae3b3c9ef63c2c2930a8e417b896b155 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 05:35:15 -0300 Subject: [PATCH 27/68] converts error codes to decimal notation --- .../sources/multisig_account.move | 6 ++-- .../tests/vote_lib/multi_action.test.move | 36 +++++++++---------- .../ol_sources/vote_lib/multi_action.move | 34 +++++++++--------- .../vote_lib/multi_action_migration.move | 6 ++-- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/framework/libra-framework/sources/multisig_account.move b/framework/libra-framework/sources/multisig_account.move index b29127d47..4cfdcc474 100644 --- a/framework/libra-framework/sources/multisig_account.move +++ b/framework/libra-framework/sources/multisig_account.move @@ -65,7 +65,7 @@ module diem_framework::multisig_account { // Any error codes > 2000 can be thrown as part of transaction prologue. /// Owner list cannot contain the same address more than once. - const EDUPLICATE_OWNER: u64 = 0x1; + const EDUPLICATE_OWNER: u64 = 1; /// Specified account is not a multisig account. const EACCOUNT_NOT_MULTISIG: u64 = 2002; /// Account executing this operation is not an owner of the multisig account. @@ -87,7 +87,7 @@ module diem_framework::multisig_account { /// Payload hash must be exactly 32 bytes (sha3-256). const EINVALID_PAYLOAD_HASH: u64 = 12; /// The multisig account itself cannot be an owner. - const EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF: u64 = 0x13; + const EOWNER_CANNOT_BE_MULTISIG_ACCOUNT_ITSELF: u64 = 13; /// Multisig accounts has not been enabled on this current network yet. const EMULTISIG_ACCOUNTS_NOT_ENABLED_YET: u64 = 14; /// The number of metadata keys and values don't match. @@ -97,7 +97,7 @@ module diem_framework::multisig_account { /// The sequence number provided is invalid. It must be between [1, next pending transaction - 1]. const EINVALID_SEQUENCE_NUMBER: u64 = 17; /// The owner does not exist in the chain. - const EOWNER_DOES_NOT_EXIST: u64 = 0x18; + const EOWNER_DOES_NOT_EXIST: u64 = 18; /// Represents a multisig account's configurations and transactions. /// This will be stored in the multisig account (created as a resource account separate from any owner accounts). diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 1033f598e..ac989fae8 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -360,7 +360,7 @@ module ol_framework::test_multi_action { // Try to propose offer to an multisig account #[test(root = @ol_framework, dave = @0x1000d, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x30019, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x30013, location = ol_framework::multi_action)] fun propose_offer_to_multisign(root: &signer, carol: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 4); let carol_address = @0x1000c; @@ -380,7 +380,7 @@ module ol_framework::test_multi_action { // Try to propose an empty offer #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x10016, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x10010, location = ol_framework::multi_action)] fun propose_empty_offer(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 4); multi_action::init_gov(alice); @@ -389,7 +389,7 @@ module ol_framework::test_multi_action { // Try to propose too many authorities #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x10024, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x10018, location = ol_framework::multi_action)] fun propose_too_many_authorities(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 1); multi_action::init_gov(alice); @@ -410,7 +410,7 @@ module ol_framework::test_multi_action { // Try to propose offer to the signer address #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x10013, location = ol_framework::multisig_account)] + #[expected_failure(abort_code = 0x1000D, location = ol_framework::multisig_account)] fun propose_offer_to_signer(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 1); let alice_address = signer::address_of(alice); @@ -433,7 +433,7 @@ module ol_framework::test_multi_action { // Try to propose offer to an invalid signer #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x60018, location = ol_framework::multisig_account)] + #[expected_failure(abort_code = 0x60012, location = ol_framework::multisig_account)] fun offer_to_invalid_authority(root: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); @@ -447,7 +447,7 @@ module ol_framework::test_multi_action { // Try to propose offer with zero duration epochs #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x10022, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x10016, location = ol_framework::multi_action)] fun offer_with_zero_duration(root: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); @@ -460,7 +460,7 @@ module ol_framework::test_multi_action { // Try to claim offer not offered to signer #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x60020, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x60014, location = ol_framework::multi_action)] fun claim_offer_not_offered(root: &signer, alice: &signer, carol: &signer) { let _vals = mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; @@ -475,7 +475,7 @@ module ol_framework::test_multi_action { // Try to claim expired offer #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x20015, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x2000F, location = ol_framework::multi_action)] fun claim_expired_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; @@ -500,7 +500,7 @@ module ol_framework::test_multi_action { // Try to claim offer of an account without proposal #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x60017, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x60011, location = ol_framework::multi_action)] fun claim_offer_without_proposal(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 2); let bob_address = @0x1000c; @@ -509,7 +509,7 @@ module ol_framework::test_multi_action { // Try to claim offer twice #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x80023, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x80017, location = ol_framework::multi_action)] fun claim_offer_twice(root: &signer, carol: &signer, alice: &signer, bob: &signer) { let _vals = mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; @@ -536,7 +536,7 @@ module ol_framework::test_multi_action { // Try to finalize account without offer #[test(root = @ol_framework, alice = @0x1000a)] - #[expected_failure(abort_code = 0x30018, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x30012, location = ol_framework::multi_action)] fun finalize_without_offer(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 1); multi_action::init_gov(alice); @@ -545,7 +545,7 @@ module ol_framework::test_multi_action { // Try to finalize account without enough offer claimed #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x30018, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x30012, location = ol_framework::multi_action)] fun finalize_without_enough_claimed(root: &signer, alice: &signer, bob: &signer, carol: &signer) { let _vals = mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; @@ -566,7 +566,7 @@ module ol_framework::test_multi_action { // Try to finalize account already finalized #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x80019, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x80013, location = ol_framework::multi_action)] fun finalize_already_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer) { let _vals = mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; @@ -757,7 +757,7 @@ module ol_framework::test_multi_action { // Try to vote on a closed ballot #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - #[expected_failure(abort_code = 0x30012, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x3000C, location = ol_framework::multi_action)] fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, dave: &signer) { // Scenario: Testing that if an action expires voting cannot be done. @@ -996,7 +996,7 @@ module ol_framework::test_multi_action { // Try to vote an invalid address for new authority #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x60018, location = ol_framework::multisig_account)] + #[expected_failure(abort_code = 0x60012, location = ol_framework::multisig_account)] fun governance_vote_invalid_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { let _vals = mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; @@ -1043,7 +1043,7 @@ module ol_framework::test_multi_action { // Try to vote multisig account address for new authority #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - #[expected_failure(abort_code = 0x10013, location = ol_framework::multisig_account)] + #[expected_failure(abort_code = 0x1000D, location = ol_framework::multisig_account)] fun governance_vote_multisig_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { let _vals = mock::genesis_n_vals(root, 5); let alice_address = @0x1000a; @@ -1064,7 +1064,7 @@ module ol_framework::test_multi_action { // Try to vote an owner as new authority #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x10026, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x1001A, location = ol_framework::multi_action)] fun governance_vote_owner_as_new_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { let _vals = mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; @@ -1085,7 +1085,7 @@ module ol_framework::test_multi_action { // Try to vote remove an authority that is not in the multisig #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 0x60027, location = ol_framework::multi_action)] + #[expected_failure(abort_code = 0x6001B, location = ol_framework::multi_action)] fun governance_vote_remove_non_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { let _vals = mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index fbf297386..5a4b93bb7 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -48,11 +48,11 @@ module ol_framework::multi_action { #[test_only] friend ol_framework::test_multi_action; - const EGOV_NOT_INITIALIZED: u64 = 0x1; + const EGOV_NOT_INITIALIZED: u64 = 1; /// The owner of this account can't be an authority, since it will subsequently be bricked. The signer of this account is no longer useful. The account is now controlled by the Governance logic. - const ESIGNER_CANT_BE_AUTHORITY: u64 = 0x2; + const ESIGNER_CANT_BE_AUTHORITY: u64 = 2; /// signer not authorized to approve a transaction. - const ENOT_AUTHORIZED: u64 = 0x3; + const ENOT_AUTHORIZED: u64 = 3; /// There are no pending transactions to search const EPENDING_EMPTY: u64 = 4; /// Not enough signers configured @@ -70,37 +70,37 @@ module ol_framework::multi_action { /// Proposal is expired const EPROPOSAL_NOT_FOUND: u64 = 11; /// Proposal voting is closed - const EVOTING_CLOSED: u64 = 0x12; + const EVOTING_CLOSED: u64 = 12; /// No addresses in multisig changes const EEMPTY_ADDRESSES: u64 = 13; /// Duplicate vote const EDUPLICATE_VOTE: u64 = 14; /// Offer expired - const EOFFER_EXPIRED: u64 = 0x15; + const EOFFER_EXPIRED: u64 = 15; /// Offer empty - const EOFFER_EMPTY: u64 = 0x16; + const EOFFER_EMPTY: u64 = 16; /// Not offered to initial authorities - const ENOT_OFFERED: u64 = 0x17; + const ENOT_OFFERED: u64 = 17; /// Not enough claimed authorities - const ENOT_ENOUGH_CLAIMED: u64 = 0x18; + const ENOT_ENOUGH_CLAIMED: u64 = 18; /// Account is already a multisig - const EALREADY_MULTISIG: u64 = 0x19; + const EALREADY_MULTISIG: u64 = 19; /// Address not proposed for authority role - const EADDRESS_NOT_PROPOSED: u64 = 0x20; + const EADDRESS_NOT_PROPOSED: u64 = 20; /// Address proposed for authority role does not exist - const EPROPOSED_NOT_EXISTS: u64 = 0x21; + const EPROPOSED_NOT_EXISTS: u64 = 21; /// Offer duration must be greater than zero - const EZERO_DURATION: u64 = 0x22; + const EZERO_DURATION: u64 = 22; /// Offer already claimed - const EALREADY_CLAIMED: u64 = 0x23; + const EALREADY_CLAIMED: u64 = 23; /// Too many addresses in offer - avoid DoS attack - const ETOO_MANY_ADDRESSES: u64 = 0x24; + const ETOO_MANY_ADDRESSES: u64 = 24; /// Offer already exists - const EOFFER_ALREADY_EXISTS: u64 = 0x25; + const EOFFER_ALREADY_EXISTS: u64 = 25; /// Already an owner - const EALREADY_OWNER: u64 = 0x26; + const EALREADY_OWNER: u64 = 26; /// Owner not found - const EOWNER_NOT_FOUND: u64 = 0x27; + const EOWNER_NOT_FOUND: u64 = 27; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move index 2a6ea317b..372e2ef5f 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move @@ -12,11 +12,11 @@ module ol_framework::multi_action_migration { //friend ol_framework::test_multi_action_migration; /// Account already has Offer structure - const EOFFER_ALREADY_EXISTS: u64 = 0x1; + const EOFFER_ALREADY_EXISTS: u64 = 1; /// Signer is not in the authorities list - const ENOT_AUTHORIZED: u64 = 0x2; + const ENOT_AUTHORIZED: u64 = 2; /// Governance is not initialized - const EGOV_NOT_INITIALIZED: u64 = 0x3; + const EGOV_NOT_INITIALIZED: u64 = 3; // DANGER - may forge the signer of the multisig account is necessary here // TODO: remove this function after offer migration is completed From 6455c7005bffb54409d281f8ccfd08d97636282c Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 06:05:55 -0300 Subject: [PATCH 28/68] sets expire epoch to use offer migration --- .../vote_lib/multi_action_migration.test.move | 20 ++++++++++++++++++ .../vote_lib/multi_action_migration.move | 21 ++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action_migration.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action_migration.test.move index 9c03db6cd..4db54477c 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action_migration.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action_migration.test.move @@ -107,4 +107,24 @@ module ol_framework::test_multi_action_migration { // try to migrate offer multi_action_migration::migrate_offer(bob, alice_address); } + + // Try to migrate offer after expire epoch + #[test(root = @ol_framework, alice = @0x1000a)] + #[expected_failure(abort_code = 0x30004, location = ol_framework::multi_action_migration)] + fun migrate_offer_after_expire_epoch(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 2); + let alice_address = signer::address_of(alice); + + // initialize the multi_action account + multi_action::init_gov_deprecated(alice); + + let i = 0; + while (i < 110) { + mock::trigger_epoch(root); + i = i + 1; + }; + + // try to migrate offer + multi_action_migration::migrate_offer(alice, alice_address); + } } \ No newline at end of file diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move index 372e2ef5f..38bbee7c3 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action_migration.move @@ -1,22 +1,36 @@ +/// The `multi_action_migration` module is designed to initialize the `Offer` structure +/// in accounts that have initiated Governance or are already multisig accounts. +/// +/// Due to the necessity of forging the signer to add structures to multisig accounts, +/// this module is separated for security reasons and will be deprecated once the migration +/// is completed by the account owners. +/// +/// Migration will be allowed for a window of ~30 epochs. After 30 epochs, the migration +/// will no longer function. Upon completion of the migration, a PR should be made to +/// remove the migration code, including the code that forges the signer. + module ol_framework::multi_action_migration { use std::signer; use std::error; use diem_framework::create_signer::create_signer; use diem_framework::multisig_account; use ol_framework::multi_action; + use ol_framework::epoch_helper; #[test_only] friend ol_framework::test_multi_action; - //#[text_only] - //friend ol_framework::test_multi_action_migration; - /// Account already has Offer structure const EOFFER_ALREADY_EXISTS: u64 = 1; /// Signer is not in the authorities list const ENOT_AUTHORIZED: u64 = 2; /// Governance is not initialized const EGOV_NOT_INITIALIZED: u64 = 3; + /// Migration expired + const EMIGRATION_EXPIRED: u64 = 4; + + /// Epoch to expire the migration + const EPOCH_TO_EXPIRE: u64 = 110; // DANGER - may forge the signer of the multisig account is necessary here // TODO: remove this function after offer migration is completed @@ -24,6 +38,7 @@ module ol_framework::multi_action_migration { public entry fun migrate_offer(sig: &signer, multisig_address: address) { // Ensure the account does not have Offer structure assert!(!multi_action::exists_offer(multisig_address), error::already_exists(EOFFER_ALREADY_EXISTS)); + assert!(epoch_helper::get_current_epoch() < EPOCH_TO_EXPIRE, error::invalid_state(EMIGRATION_EXPIRED)); // if account is multisig, forge signer and add Offer to the multisig account if (multisig_account::is_multisig(multisig_address)) { From 621dbbbc2b2358b6555342e42fa8de72764f10ea Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 06:53:44 -0300 Subject: [PATCH 29/68] converts exist_offer into a view function --- .../sources/multisig_account.move | 2 -- .../tests/vote_lib/multi_action.test.move | 14 +++++---- .../ol_sources/vote_lib/multi_action.move | 30 ++++++++++--------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/framework/libra-framework/sources/multisig_account.move b/framework/libra-framework/sources/multisig_account.move index 4cfdcc474..c4675e7ec 100644 --- a/framework/libra-framework/sources/multisig_account.move +++ b/framework/libra-framework/sources/multisig_account.move @@ -598,8 +598,6 @@ module diem_framework::multisig_account { ); } - - /// Similar to add_owners, but only allow adding one owner. entry fun add_owner(multisig_account: &signer, new_owner: address) acquires MultisigAccount { add_owners(multisig_account, vector[new_owner]); diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index ac989fae8..8df401efc 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -39,9 +39,11 @@ module ol_framework::test_multi_action { // check the offer does not exist assert!(!multi_action::exists_offer(carol_address), 7357001); assert!(!multi_action::is_multi_action(carol_address), 7357002); + assert!(!multi_action::exists_offer(carol_address), 7357003); // initialize the multi_action account multi_action::init_gov(carol); + assert!(multi_action::exists_offer(carol_address), 7357004); // offer authorities let authorities = vector::empty
(); @@ -50,15 +52,15 @@ module ol_framework::test_multi_action { multi_action::propose_offer(carol, authorities, option::none()); // check the offer is proposed and account is not muti_action yet - assert!(multi_action::exists_offer(carol_address), 7357003); - assert!(multi_action::get_offer_proposed(carol_address) == authorities, 7357004); - assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); - assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 7357006); + assert!(multi_action::exists_offer(carol_address), 7357005); + assert!(multi_action::get_offer_proposed(carol_address) == authorities, 7357006); + assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357007); + assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 7357008); let expiration = vector::empty(); vector::push_back(&mut expiration, 7); vector::push_back(&mut expiration, 7); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == expiration, 7357007); - assert!(!multi_action::is_multi_action(carol_address), 7357008); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == expiration, 7357009); + assert!(!multi_action::is_multi_action(carol_address), 7357010); } // Propose new offer after expired diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 5a4b93bb7..bd26f2e40 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -443,14 +443,22 @@ module ol_framework::multi_action { } //////// Helper functions to check initialization ////////// + #[view] - /// Is the Multisig Governance initialized? + /// Is the Multisig Governance finalized public fun is_multi_action(addr: address): bool { exists(addr) && exists>(addr) && multisig_account::is_multisig(addr) } + // Check if the account has the Governance initialized + #[view] + public fun is_gov_init(addr: address): bool { + exists(addr) && + exists>(addr) + } + /// helper to assert if the account is in the right state fun assert_multi_action(addr: address) { assert!(multisig_account::is_multisig(addr), error::invalid_argument(ENOT_FINALIZED_NOT_BRICK)); @@ -458,13 +466,8 @@ module ol_framework::multi_action { assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); } - // Check if the account is a multisig and has the Governance initialized - public fun is_gov_init(addr: address): bool { - exists(addr) && - exists>(addr) - } - // Query if an offer exists for the given multisig address. + #[view] public fun exists_offer(multisig_address: address): bool { exists(multisig_address) } @@ -719,12 +722,6 @@ module ol_framework::multi_action { epoch_now > prop.expiration_epoch } - #[view] - public fun is_authority(multisig_addr: address, addr: address): bool { - let auths = multisig_account::owners(multisig_addr); - vector::contains(&auths, &addr) - } - /// This function is used to copy the data from the proposal that is in the multisig. /// Note that this is the only way to get the data out of the multisig, and it is the only function to use the `copy` trait. If you have a workflow that needs copying, then the data struct for the action payload will need to use the `copy` trait. public(friend) fun extract_proposal_data(multisig_address: address, uid: &guid::ID): ProposalData acquires Action { @@ -928,6 +925,12 @@ module ol_framework::multi_action { multisig_account::owners(multisig_address) } + #[view] + public fun is_authority(multisig_addr: address, addr: address): bool { + let auths = multisig_account::owners(multisig_addr); + vector::contains(&auths, &addr) + } + #[view] public fun get_threshold(multisig_address: address): (u64, u64) { (multisig_account::num_signatures_required(multisig_address), vector::length(&multisig_account::owners(multisig_address))) @@ -949,7 +952,6 @@ module ol_framework::multi_action { }) } - #[view] /// returns the votes for a given proposal ID. For `view` functions must provide the destructured guid::ID as address and integer. public fun get_votes(multisig_address: address, id_num: u64): vector
acquires Action { From d54965a385f270ccafa8f1f58cbc13e9bab3850e Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:14:35 -0300 Subject: [PATCH 30/68] comments code not used and not tested --- .../sources/ol_sources/vote_lib/multi_action.move | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index bd26f2e40..df879f41e 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -753,6 +753,7 @@ module ol_framework::multi_action { return (true, guid, idx, ballot::get_pending_enum(), complete) }; + /* code not used and not tested let (found, guid, idx) = find_index_of_ballot_by_data(tracker, data, ballot::get_approved_enum()); if (found) { let b = ballot::get_ballot_by_id(tracker, &guid); @@ -765,7 +766,7 @@ module ol_framework::multi_action { let b = ballot::get_ballot_by_id(tracker, &guid); let complete = ballot::is_completed>(b); return (true, guid, idx, ballot::get_rejected_enum(), complete) - }; + };*/ (false, guid::create_id(@0x0, 0), 0, 0, false) } From 0162fec49cd84991a08a87954e5e786b99bd4ab8 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:16:57 -0300 Subject: [PATCH 31/68] fixes indentation --- .../sources/ol_sources/vote_lib/multi_action.move | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index df879f41e..93fc98c00 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -800,8 +800,8 @@ module ol_framework::multi_action { } = t; if (existing_data == incoming_data) { - let uid = ballot::get_ballot_id(b); - return (true, uid, i) + let uid = ballot::get_ballot_id(b); + return (true, uid, i) }; i = i + 1; }; From 746718f79a3cd2fd333e677045e5e6749c50605b Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:25:18 -0300 Subject: [PATCH 32/68] adds test try to vote twice on the same ballot --- .../tests/vote_lib/multi_action.test.move | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 8df401efc..944f69ede 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -757,7 +757,7 @@ module ol_framework::test_multi_action { multi_action::maybe_restore_withdraw_cap(cap_opt); } - // Try to vote on a closed ballot + // Try to vote on an expired ballot #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] #[expected_failure(abort_code = 0x3000C, location = ol_framework::multi_action)] fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, dave: &signer) { @@ -800,9 +800,9 @@ module ol_framework::test_multi_action { // trying to vote on a closed ballot will error let _passed = multi_action::vote_governance(bob, erik_address, &id); - } - + } + // Happy day: change the authorities of a multisig #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] fun governance_change_auths(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { // Scenario: The multisig gets initiated with the 2 validators as the only authorities. IT takes 2-of-2 to sign. @@ -996,6 +996,42 @@ module ol_framework::test_multi_action { assert!(multi_action::get_offer_expiration_epoch(alice_address) == expiration, 7357005); } + // Try to vote twice on the same ballot + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x3000C, location = ol_framework::multi_action)] + fun vote_action_twice(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + // Scenario: Testing that a vote cannot be done twice on the same ballot. + + let _vals = mock::genesis_n_vals(root, 4); + mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); + let carol_address = @0x1000c; + let dave_address = @0x1000d; + ol_account::transfer(alice, carol_address, 100); + + // offer alice and bob authority on the safe + multi_action::init_gov(carol); + multi_action::init_type(carol, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(alice)); + vector::push_back(&mut authorities, signer::address_of(bob)); + multi_action::propose_offer(carol, authorities, option::none()); + multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(bob, carol_address); + multi_action::finalize_and_cage2(carol); + + // alice is going to propose to change the authorities to add dave + let id = multi_action::propose_governance(alice, carol_address, + vector::singleton(dave_address), true, option::none(), + option::none()); + + // bob votes + let passed = multi_action::vote_governance(bob, carol_address, &id); + assert!(passed, 7357002); + + // bob try to vote again + let _passed = multi_action::vote_governance(bob, carol_address, &id); + } + // Try to vote an invalid address for new authority #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 0x60012, location = ol_framework::multisig_account)] From 96e0bd9c4732fa34ef9d4254efe94c6de99996d5 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:53:09 -0300 Subject: [PATCH 33/68] applies offer flows to safe, community wallet and donor voice - WIP --- .../ol_sources/community_wallet_init.move | 46 ++--- .../tests/community_wallet.test.move | 167 +++++++++--------- .../ol_sources/tests/donor_voice.test.move | 150 ++++++++++------ .../tests/vote_lib/multi_action.test.move | 55 +++--- .../ol_sources/tests/vote_lib/safe.test.move | 38 +++- .../ol_sources/vote_lib/donor_voice_txs.move | 11 +- .../ol_sources/vote_lib/multi_action.move | 48 +++-- 7 files changed, 294 insertions(+), 221 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/community_wallet_init.move b/framework/libra-framework/sources/ol_sources/community_wallet_init.move index 464a13308..f1eb5c44c 100644 --- a/framework/libra-framework/sources/ol_sources/community_wallet_init.move +++ b/framework/libra-framework/sources/ol_sources/community_wallet_init.move @@ -49,9 +49,7 @@ module ol_framework::community_wallet_init { /// minimum m authorities for a wallet const MINIMUM_AUTH: u64 = 3; - - public(friend) fun migrate_community_wallet_account(framework: &signer, dv_account: - &signer) { + public(friend) fun migrate_community_wallet_account(framework: &signer, dv_account: &signer) { use diem_framework::system_addresses; system_addresses::assert_diem_framework(framework); donor_voice_txs::migrate_community_wallet_account(framework, dv_account); @@ -60,29 +58,30 @@ module ol_framework::community_wallet_init { //////// MULTISIG TX HELPERS //////// - // Helper to initialize the PaymentMultiAction but also while confirming that the signers are not related family - // These transactions can be sent directly to donor_voice, but this is a helper to make it easier to initialize the multisig with the ancestry requirements. - + // Helper to initialize: + // - the PaymentMultiAction + // - offer authorities to the multisig after confirming that the signers are not related family + // These transactions can be sent directly to donor_voice, but this is a helper to make it easier to initialize + // the multisig with the ancestry requirements. public entry fun init_community( sig: &signer, - check_addresses: vector
, + initial_authorities: vector
, check_threshold: u64, ) { - check_proposed_auths(check_addresses, check_threshold); + check_proposed_auths(initial_authorities, check_threshold); donor_voice_txs::make_donor_voice(sig); if (!donor_voice_txs::is_liquidate_to_match_index(signer::address_of(sig))) { donor_voice_txs::set_liquidate_to_match_index(sig, true); }; match_index::opt_into_match_index(sig); - + + propose_offer(sig, initial_authorities, check_threshold); } #[view] - /// check if the authorities being proposed, and signature threshold would - /// qualify - public fun check_proposed_auths(initial_authorities: vector
, num_signers: - u64): bool { + /// check if the authorities being proposed, and signature threshold would qualify + public fun check_proposed_auths(initial_authorities: vector
, num_signers: u64): bool { // TODO: enforce n/m multi auth such as: // let n = if (len == 3) { 2 } @@ -92,35 +91,36 @@ module ol_framework::community_wallet_init { assert!(num_signers >= MINIMUM_SIGS, error::invalid_argument(ESIG_THRESHOLD_CONFIG)); - // policy is to have at least m signers as auths on the account. + // policy is to have at least m signers as auths on the account. let len = vector::length(&initial_authorities); assert!(len >= MINIMUM_AUTH, error::invalid_argument(ETOO_FEW_AUTH)); let (fam, _, _) = ancestry::any_family_in_list(initial_authorities); assert!(!fam, error::invalid_argument(ESIGNERS_SYBIL)); true + } + /// Propose offer to the multisig, and check if the signers are not related family + public entry fun propose_offer(sig: &signer, new_signers: vector
, num_signers: u64) { + check_proposed_auths(new_signers, num_signers); + multi_action::propose_offer_internal(sig, new_signers, option::none()); } /// convenience function to check if the account can be caged /// after all the structs are in place - public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { + public entry fun finalize_and_cage(sig: &signer, num_signers: u64) { let addr = signer::address_of(sig); - assert!(donor_voice_txs::is_liquidate_to_match_index(addr), error::invalid_argument(ENOT_MATCH_INDEX_LIQ)); - - multi_action::finalize_and_cage(sig, initial_authorities, num_signers); + multi_action::finalize_and_cage(sig, num_signers); community_wallet::set_comm_wallet(sig); - + + assert!(donor_voice_txs::is_liquidate_to_match_index(addr), error::invalid_argument(ENOT_MATCH_INDEX_LIQ)); assert!(multisig_thresh(addr), error::invalid_argument(ESIG_THRESHOLD_RATIO)); - assert!(!multisig_common_ancestry(addr), - error::invalid_argument(ESIGNERS_SYBIL)); + assert!(!multisig_common_ancestry(addr), error::invalid_argument(ESIGNERS_SYBIL)); assert!(community_wallet::is_init(addr), error::invalid_argument(ENO_CW_FLAG)); - } #[view] - /// Dynamic check to see if CommunityWallet is qualifying. /// if it is not qualifying it wont be part of the burn funds matching. public fun qualifies(addr: address): bool { diff --git a/framework/libra-framework/sources/ol_sources/tests/community_wallet.test.move b/framework/libra-framework/sources/ol_sources/tests/community_wallet.test.move index cb4bc2602..6eec3717f 100644 --- a/framework/libra-framework/sources/ol_sources/tests/community_wallet.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/community_wallet.test.move @@ -5,6 +5,7 @@ use ol_framework::community_wallet; use ol_framework::community_wallet_init; use ol_framework::donor_voice_txs; + use ol_framework::multi_action; use diem_framework::multisig_account; use ol_framework::mock; use ol_framework::ol_account; @@ -12,18 +13,15 @@ use std::signer; use std::vector; + // use diem_std::debug::print; - // use diem_std::debug::print; - - - #[test(root = @ol_framework, community = @0x10011)] - fun migrate_cw_bug_not_resource(root: &signer, community: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, community = @0x10011)] + fun migrate_cw_bug_not_resource(root: &signer, alice: &signer, bob: &signer, carol: &signer, community: &signer) { // create genesis and fund accounts let auths = mock::genesis_n_vals(root, 3); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); - let community_wallet_address = signer::address_of(community); // genesis migration would have created this account. ol_account::create_account(root, community_wallet_address); @@ -39,8 +37,13 @@ // confirm the bug assert!(!multisig_account::is_multisig(community_wallet_address), 7357002); + // vals claim the offer + multi_action::claim_offer(alice, community_wallet_address); + multi_action::claim_offer(bob, community_wallet_address); + multi_action::claim_offer(carol, community_wallet_address); + // fix it by calling multi auth: - community_wallet_init::finalize_and_cage(community, auths, vector::length(&auths)); + community_wallet_init::finalize_and_cage(community, vector::length(&auths)); // multi_action::finalize_and_cage(community); assert!(multisig_account::is_multisig(community_wallet_address), 7357003); @@ -92,7 +95,7 @@ fun cw_decrease_below_m_authorized_sigs(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer, eve: &signer ) { // A community wallet by default must be 2/3 multisig. // This test verifies that the wallet can not be initialized with less signers - mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 5); mock::ol_initialize_coin_and_fund_vals(root, 1000, true); let (_, carol_balance_pre) = ol_account::balance(@0x1000c); @@ -112,11 +115,15 @@ vector::push_back(&mut signers, signer::address_of(bob)); vector::push_back(&mut signers, signer::address_of(dave)); vector::push_back(&mut signers, signer::address_of(eve)); - community_wallet_init::init_community(alice, signers, 2); + // signers claim the offer + multi_action::claim_offer(bob, signer::address_of(alice)); + multi_action::claim_offer(dave, signer::address_of(alice)); + multi_action::claim_offer(eve, signer::address_of(alice)); + // fix it by calling multi auth: - community_wallet_init::finalize_and_cage(alice, signers, 2); + community_wallet_init::finalize_and_cage(alice, 2); let alice_comm_wallet_addr = signer::address_of(alice); let carols_addr = signer::address_of(carol); @@ -167,7 +174,7 @@ fun cw_decrease_below_minimum_n_sigs(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer, eve: &signer ) { // A community wallet by default must be 2/3 multisig. // This test verifies that the wallet can not be initialized with less signers - mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 5); mock::ol_initialize_coin_and_fund_vals(root, 1000, true); let (_, carol_balance_pre) = ol_account::balance(@0x1000c); @@ -187,11 +194,15 @@ vector::push_back(&mut signers, signer::address_of(bob)); vector::push_back(&mut signers, signer::address_of(dave)); vector::push_back(&mut signers, signer::address_of(eve)); - community_wallet_init::init_community(alice, signers, 2); + // signers claim the offer + multi_action::claim_offer(bob, signer::address_of(alice)); + multi_action::claim_offer(dave, signer::address_of(alice)); + multi_action::claim_offer(eve, signer::address_of(alice)); + // fix it by calling multi auth: - community_wallet_init::finalize_and_cage(alice, signers, 2); + community_wallet_init::finalize_and_cage(alice, 2); let alice_comm_wallet_addr = signer::address_of(alice); let carols_addr = signer::address_of(carol); @@ -236,112 +247,106 @@ } + // Try to initialize with less than the required signitures #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 65542, location = 0x1::community_wallet_init)] - fun cw_init_with_n_and_m_below_minimum_sigs(root: &signer, alice: &signer, bob: &signer) { + #[expected_failure(abort_code = 0x10005, location = 0x1::community_wallet_init)] + fun cw_init_with_less_signitures_than_min(root: &signer, alice: &signer) { // A community wallet by default must be 2/3 multisig. - // This test verifies that the wallet can not be initialized with less signers mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 1000, true); - - // create signers - let signers = vector::empty
(); - vector::push_back(&mut signers, signer::address_of(bob)); - - - community_wallet_init::migrate_community_wallet_account(root, alice); - - donor_voice_txs::test_helper_make_donor_voice(root, alice); - - // try to cage the address by calling multi auth - community_wallet_init::finalize_and_cage(alice, signers, 1); - + // try to initialize + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + community_wallet_init::init_community(alice, authorities, 1); } + // Try to initialize with less than the required authorities #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - #[expected_failure(abort_code = 65542, location = 0x1::community_wallet_init)] - fun cw_dd_init_with_n_below_minimum_sigs(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + #[expected_failure(abort_code = 0x10009, location = 0x1::community_wallet_init)] + fun cw_init_with_less_authorities_than_min(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // A community wallet by default must be 2/3 multisig. - // This test verifies that the wallet can not be decreased below this specification mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 1000, true); - // create signers + // try to initialize let signers = vector::empty
(); vector::push_back(&mut signers, signer::address_of(bob)); vector::push_back(&mut signers, signer::address_of(carol)); - vector::push_back(&mut signers, signer::address_of(dave)); + community_wallet_init::init_community(alice, signers, 2); + } - community_wallet_init::migrate_community_wallet_account(root, alice); + // Try to finalize with less than the required authorities + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x10006, location = 0x1::community_wallet_init)] + fun cw_finalize_with_less_authorities_than_min(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + // A community wallet by default must be 2/3 multisig. + mock::genesis_n_vals(root, 4); + mock::ol_initialize_coin_and_fund_vals(root, 1000, true); - donor_voice_txs::test_helper_make_donor_voice(root, alice); + let authorities = vector::singleton(@0x1000b); + vector::push_back(&mut authorities, @0x1000c); + vector::push_back(&mut authorities, @0x1000d); + community_wallet_init::init_community(alice, authorities, 2); - // try to cage the address by calling multi auth - community_wallet_init::finalize_and_cage(alice, signers, 1); + multi_action::claim_offer(bob, signer::address_of(alice)); + multi_action::claim_offer(carol, signer::address_of(alice)); + // try to finalize + community_wallet_init::finalize_and_cage(alice, 2); } + // Try to finalize with less than the required signitures #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - #[expected_failure(abort_code = 65542, location = 0x1::community_wallet_init)] - fun cw_dd_decrease_m_below_minimum_sigs(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + #[expected_failure(abort_code = 0x10006, location = 0x1::community_wallet_init)] + fun cw_finalize_with_less_signitures_than_min(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { // A community wallet by default must be 2/3 multisig. - // This test verifies that the wallet can not be decreased below this specification mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 1000, true); - - // create signers let signers = vector::empty
(); vector::push_back(&mut signers, signer::address_of(bob)); vector::push_back(&mut signers, signer::address_of(carol)); vector::push_back(&mut signers, signer::address_of(dave)); - community_wallet_init::migrate_community_wallet_account(root, alice); - - donor_voice_txs::test_helper_make_donor_voice(root, alice); + community_wallet_init::init_community(alice, signers, 2); - // try to cage the address by calling multi auth - community_wallet_init::finalize_and_cage(alice, signers, 1); + multi_action::claim_offer(bob, signer::address_of(alice)); + multi_action::claim_offer(carol, signer::address_of(alice)); + multi_action::claim_offer(dave, signer::address_of(alice)); + // try to finalize + community_wallet_init::finalize_and_cage(alice, 1); } + // change the authorities offer of a community wallet + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, eve = @0x1000e)] + fun cw_change_authorities(root: &signer, alice: &signer, bob: &signer, carol: &signer,dave: &signer, eve: &signer) { + mock::genesis_n_vals(root, 5); + mock::ol_initialize_coin_and_fund_vals(root, 1000, true); + let authorities = vector::empty
(); + vector::push_back(&mut authorities, signer::address_of(bob)); + vector::push_back(&mut authorities, signer::address_of(carol)); + vector::push_back(&mut authorities, signer::address_of(dave)); + community_wallet_init::init_community(alice, authorities, 2); - // TODO: test below - - // #[test(root = @ol_framework, _alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, eve = @0x1000e)] - // #[expected_failure(abort_code = 196618, location = 0x1::ol_account)] - // fun cw_below_minimum_signer(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer, eve: &signer) { - // // A community wallet by default must be 2/3 multisig. - // // This test verifies that the wallet can not be initialized with less signers - // mock::genesis_n_vals(root, 4); - // mock::ol_initialize_coin_and_fund_vals(root, 1000, true); - - // let signers = vector::empty
(); - - // // helpers in line to help - // vector::push_back(&mut signers, signer::address_of(bob)); - // vector::push_back(&mut signers, signer::address_of(dave)); - - // community_wallet_init::init_community(alice, signers); - - // // After being set as a community wallet, the owner loses control over the wallet - // ol_account::transfer(alice, @0x1000b, 100); - // } - - // let alice_comm_wallet_addr = signer::address_of(alice); - // let carols_addr = signer::address_of(carol); - - // let uid = donor_voice_txs::test_propose_payment(bob, alice_comm_wallet_addr, carols_addr, 100, b"thanks carol"); - // let (found, idx, status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(alice_comm_wallet_addr, &uid); - // assert!(found, 7357004); - // assert!(idx == 0, 7357005); - // assert!(status_enum == 1, 7357006); - // assert!(!completed, 7357007); + vector::pop_back(&mut authorities); // remove dave + vector::push_back(&mut authorities, signer::address_of(eve)); // add eve + community_wallet_init::propose_offer(alice, authorities, 2); - // // it is not yet scheduled, it's still only a proposal by an admin - // assert!(!donor_voice_txs::is_scheduled(alice_comm_wallet_addr, &uid), 7357008); + multi_action::claim_offer(bob, signer::address_of(alice)); + multi_action::claim_offer(carol, signer::address_of(alice)); + multi_action::claim_offer(eve, signer::address_of(alice)); + community_wallet_init::finalize_and_cage(alice, 2); + // certify the change + let new_authorities = multi_action::get_authorities(signer::address_of(alice)); + assert!(vector::length(&new_authorities) == 3, 7357001); + assert!(vector::contains(&new_authorities, &signer::address_of(bob)), 7357002); + assert!(vector::contains(&new_authorities, &signer::address_of(carol)), 7357003); + assert!(vector::contains(&new_authorities, &signer::address_of(eve)), 7357004); + } } diff --git a/framework/libra-framework/sources/ol_sources/tests/donor_voice.test.move b/framework/libra-framework/sources/ol_sources/tests/donor_voice.test.move index 8cdd26a97..bff2ba214 100644 --- a/framework/libra-framework/sources/ol_sources/tests/donor_voice.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/donor_voice.test.move @@ -21,17 +21,15 @@ module ol_framework::test_donor_voice { use diem_std::debug::print; - #[test(root = @ol_framework, alice = @0x1000a)] - fun dd_init(root: &signer, alice: &signer) { - mock::genesis_n_vals(root, 4); + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + fun dd_init(root: &signer, alice: &signer, bob: &signer) { + let vals = mock::genesis_n_vals(root, 2); let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); let donor_voice_address = signer::address_of(&resource_sig); - let auths = mock::personas(); - // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); let list = donor_voice::get_root_registry(); assert!(vector::length(&list) == 1, 7357001); @@ -39,8 +37,12 @@ module ol_framework::test_donor_voice { //shouldnt be donor voice yet. Needs to be caged first assert!(!donor_voice_txs::is_donor_voice(donor_voice_address), 7357002); + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); + //need to be caged to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, auths, vector::length(&auths)); + multi_action::finalize_and_cage(&resource_sig, 2); assert!(donor_voice_txs::is_donor_voice(donor_voice_address), 7357003); } @@ -50,15 +52,19 @@ module ol_framework::test_donor_voice { // Scenario: Alice creates a resource_account which will be a donor directed account. She will not be one of the authorities of the account. // only bob, carol, and dave with be authorities - let vals = mock::genesis_n_vals(root, 4); + let vals = mock::genesis_n_vals(root, 2); let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); let donor_voice_address = signer::address_of(&resource_sig); // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); + + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); //need to be caged to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); + multi_action::finalize_and_cage(&resource_sig, vector::length(&vals)); let uid = donor_voice_txs::test_propose_payment(bob, donor_voice_address, @0x1000b, 100, b"thanks bob"); let (found, idx, status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(donor_voice_address, &uid); @@ -71,8 +77,8 @@ module ol_framework::test_donor_voice { assert!(!donor_voice_txs::is_scheduled(donor_voice_address, &uid), 7357008); } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - fun dd_schedule_happy(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun dd_schedule_happy(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { // Scenario: Alice creates a resource_account which will be a donor directed account. She will not be one of the authorities of the account. // only bob, carol, and dave with be authorities @@ -81,10 +87,16 @@ module ol_framework::test_donor_voice { let donor_voice_address = signer::address_of(&resource_sig); // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); + + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); + multi_action::claim_offer(carol, donor_voice_address); + multi_action::claim_offer(dave, donor_voice_address); //need to cage to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); + multi_action::finalize_and_cage(&resource_sig, 2); let uid = donor_voice_txs::test_propose_payment(bob, donor_voice_address, @0x1000b, 100, b"thanks bob"); let (found, idx, status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(donor_voice_address, &uid); @@ -126,10 +138,17 @@ module ol_framework::test_donor_voice { let donor_voice_address = signer::address_of(&resource_sig); // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); + + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); + multi_action::claim_offer(carol, donor_voice_address); + multi_action::claim_offer(dave, donor_voice_address); + multi_action::claim_offer(eve, donor_voice_address); //need to be caged to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); + multi_action::finalize_and_cage(&resource_sig, 2); // EVE Establishes some governance over the wallet, when donating. let eve_donation = 42; @@ -144,28 +163,23 @@ module ol_framework::test_donor_voice { let is_donor = donor_voice_governance::check_is_donor(donor_voice_address, signer::address_of(dave)); assert!(is_donor, 7357003); - // Bob proposes a tx that will come from the donor directed account. // It is not yet scheduled because it doesnt have the MultiAuth quorum. Still waiting for Alice or Carol to approve. let uid_of_transfer = donor_voice_txs::test_propose_payment(bob, donor_voice_address, @0x1000b, 100, b"thanks bob"); - let (_found, _idx, _status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(donor_voice_address, &uid_of_transfer); assert!(!completed, 7357004); - // Eve wants to propose a Veto, but this should fail at this, because // the tx is not yet scheduled let _uid_of_veto_prop = donor_voice_txs::test_propose_veto(eve, &uid_of_transfer); let has_veto = donor_voice_governance::tx_has_veto(donor_voice_address, guid::id_creation_num(&uid_of_transfer)); assert!(!has_veto, 7357005); // propose does not cast a vote. - // Now Carol, along with Bob, as admins have proposed the payment. // Now the payment should be scheduled let uid_of_transfer = donor_voice_txs::test_propose_payment(carol, donor_voice_address, @0x1000b, 100, b"thanks bob"); assert!(donor_voice_txs::is_scheduled(donor_voice_address, &uid_of_transfer), 7357006); // is scheduled - // Eve tries again after it has been scheduled let _uid_of_veto_prop = donor_voice_txs::test_propose_veto(eve, &uid_of_transfer); let has_veto = donor_voice_governance::tx_has_veto(donor_voice_address, guid::id_creation_num(&uid_of_transfer)); @@ -194,16 +208,12 @@ module ol_framework::test_donor_voice { // it's vetoed assert!(donor_voice_txs::is_veto(donor_voice_address, &uid_of_transfer), 7357011); - } - // should not be able sign a tx twice - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] #[expected_failure(abort_code = 65550, location = 0x1::multi_action)] - - fun dd_reject_duplicate_proposal(root: &signer, alice: &signer, bob: - &signer) { + fun dd_reject_duplicate_proposal(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { // Scenario: Alice creates a resource_account which will be a donor directed account. She will not be one of the authorities of the account. // only bob, carol, and dave with be authorities @@ -212,13 +222,18 @@ module ol_framework::test_donor_voice { let donor_voice_address = signer::address_of(&resource_sig); // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); + + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); + multi_action::claim_offer(carol, donor_voice_address); + multi_action::claim_offer(dave, donor_voice_address); //need to cage to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); + multi_action::finalize_and_cage(&resource_sig, 2); - let uid = donor_voice_txs::test_propose_payment(bob, donor_voice_address, - @0x1000c, 100, b"thanks carol"); + let uid = donor_voice_txs::test_propose_payment(bob, donor_voice_address, @0x1000c, 100, b"thanks carol"); let (found, idx, status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(donor_voice_address, &uid); assert!(found, 7357004); @@ -229,16 +244,15 @@ module ol_framework::test_donor_voice { // it is not yet scheduled, it's still only a proposal by an admin assert!(!donor_voice_txs::is_scheduled(donor_voice_address, &uid), 7357008); - let uid = donor_voice_txs::test_propose_payment(bob, donor_voice_address, - @0x1000c, 100, b"thanks carol"); + let uid = donor_voice_txs::test_propose_payment(bob, donor_voice_address, @0x1000c, 100, b"thanks carol"); // confirm it is scheduled assert!(!donor_voice_txs::is_scheduled(donor_voice_address, &uid), 7357008); } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - fun dd_process_unit(root: &signer, alice: &signer, bob: &signer, carol: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + fun dd_process_unit(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { // Scenario: Alice creates a resource_account which will be a donor directed account. She will not be one of the authorities of the account. // only bob, carol, and dave with be authorities @@ -258,10 +272,16 @@ module ol_framework::test_donor_voice { // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); + + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); + multi_action::claim_offer(carol, donor_voice_address); + multi_action::claim_offer(dave, donor_voice_address); //need to be caged to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); + multi_action::finalize_and_cage(&resource_sig, 2); let uid = donor_voice_txs::test_propose_payment(bob, donor_voice_address, @0x1000b, 100, b"thanks bob"); @@ -319,7 +339,6 @@ module ol_framework::test_donor_voice { let (_bal, marlon_rando_balance_pre) = ol_account::balance(signer::address_of(marlon_rando)); assert!(marlon_rando_balance_pre == 0, 7357000); - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); let donor_voice_address = signer::address_of(&resource_sig); @@ -328,14 +347,16 @@ module ol_framework::test_donor_voice { let (_, resource_balance) = ol_account::balance(donor_voice_address); assert!(resource_balance == 100, 7357002); - // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); + multi_action::claim_offer(carol, donor_voice_address); //need to be caged to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); - + multi_action::finalize_and_cage(&resource_sig, 2); slow_wallet::user_set_slow(marlon_rando); let uid = donor_voice_txs::propose_payment(bob, donor_voice_address, signer::address_of(marlon_rando), 100, b"thanks marlon"); @@ -375,8 +396,8 @@ module ol_framework::test_donor_voice { assert!(marlon_rando_balance_post == marlon_rando_balance_pre + 100, 7357006); } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, marlon_rando = @0x123456)] - fun dd_process_multi_same_epoch(root: &signer, alice: &signer, bob: &signer, carol: &signer, marlon_rando: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, marlon_rando = @0x123456)] + fun dd_process_multi_same_epoch(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer, marlon_rando: &signer) { // Scenario: Alice creates a resource_account which will be a donor directed account. She will not be one of the authorities of the account. // only bob, carol, and dave with be authorities @@ -385,7 +406,6 @@ module ol_framework::test_donor_voice { let marlon_pay_one = 111; let marlon_pay_two = 222; - let vals = mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); @@ -393,7 +413,6 @@ module ol_framework::test_donor_voice { let (_bal, marlon_rando_balance_pre) = ol_account::balance(signer::address_of(marlon_rando)); assert!(marlon_rando_balance_pre == 0, 7357000); - let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); let donor_voice_address = signer::address_of(&resource_sig); @@ -402,12 +421,17 @@ module ol_framework::test_donor_voice { let (_, resource_balance) = ol_account::balance(donor_voice_address); assert!(resource_balance == 10000, 7357002); - // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); + + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); + multi_action::claim_offer(carol, donor_voice_address); + multi_action::claim_offer(dave, donor_voice_address); //need to be caged to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); + multi_action::finalize_and_cage(&resource_sig, 2); slow_wallet::user_set_slow(marlon_rando); @@ -548,10 +572,15 @@ module ol_framework::test_donor_voice { // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); + // vals claim the offer + multi_action::claim_offer(alice, donor_voice_address); + multi_action::claim_offer(bob, donor_voice_address); + multi_action::claim_offer(carol, donor_voice_address); + //need to be caged to finalize donor directed workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); + multi_action::finalize_and_cage(&resource_sig, 2); slow_wallet::user_set_slow(marlon_rando); @@ -646,7 +675,7 @@ module ol_framework::test_donor_voice { // Dave and Eve make a donation and so are able to have some voting on that account. // Dave and Eve are unhappy, and vote to liquidate the account. - mock::genesis_n_vals(root, 5); // need to include eve to init funds + let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds mock::ol_initialize_coin_and_fund_vals(root, 100000, true); // start at epoch 1, since turnout tally needs epoch info, and 0 may cause // issues @@ -656,7 +685,7 @@ module ol_framework::test_donor_voice { let donor_voice_address = signer::address_of(donor_voice); // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, donor_voice); + donor_voice_txs::test_helper_make_donor_voice(root, donor_voice, vals); // EVE Establishes some governance over the wallet, when donating. let eve_donation = 42; @@ -731,7 +760,7 @@ module ol_framework::test_donor_voice { // Dave and Eve make a donation and so are able to have some voting on that account. // Dave and Eve are unhappy, and vote to liquidate the account. - mock::genesis_n_vals(root, 5); // need to include eve to init funds + let vals = mock::genesis_n_vals(root, 5); // need to include eve to init funds mock::ol_initialize_coin_and_fund_vals(root, 100000, true); // start at epoch 1, since turnout tally needs epoch info, and 0 may cause issues mock::trigger_epoch(root); @@ -741,7 +770,7 @@ module ol_framework::test_donor_voice { let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(alice, b"0x1"); let donor_voice_address = signer::address_of(&resource_sig); // the account needs basic donor directed structs - donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig); + donor_voice_txs::test_helper_make_donor_voice(root, &resource_sig, vals); donor_voice_txs::set_liquidate_to_match_index(&resource_sig, true); // EVE Establishes some governance over the wallet, when donating. @@ -812,8 +841,8 @@ module ol_framework::test_donor_voice { assert!(lifetime_burn_now == lifetime_burn_pre, 7357011); } - #[test(root = @ol_framework, community = @0x10011)] - fun migrate_cw_bug_not_resource(root: &signer, community: &signer) { + #[test(root = @ol_framework, community = @0x10011, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] + fun migrate_cw_bug_not_resource(root: &signer, community: &signer, alice: &signer, bob: &signer, carol: &signer) { // create genesis and fund accounts let auths = mock::genesis_n_vals(root, 3); @@ -835,8 +864,13 @@ module ol_framework::test_donor_voice { // confirm the bug assert!(!multisig_account::is_multisig(community_wallet_address), 7357002); + // athorities claim the offer + multi_action::claim_offer(alice, community_wallet_address); + multi_action::claim_offer(bob, community_wallet_address); + multi_action::claim_offer(carol, community_wallet_address); + // fix it by calling multi auth: - community_wallet_init::finalize_and_cage(community, auths, 2); + community_wallet_init::finalize_and_cage(community, 2); assert!(multisig_account::is_multisig(community_wallet_address), 7357003); community_wallet_init::assert_qualifies(community_wallet_address); diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 944f69ede..936417344 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -150,7 +150,7 @@ module ol_framework::test_multi_action { // finalize the multi_action account assert!(account::exists_at(carol_address), 7357001); - multi_action::finalize_and_cage2(carol); + multi_action::finalize_and_cage(carol, 2); // check the account is multi_action assert!(multi_action::is_multi_action(carol_address), 7357002); @@ -190,7 +190,7 @@ module ol_framework::test_multi_action { // finalize the multi_action account assert!(account::exists_at(carol_address), 7357001); - multi_action::finalize_and_cage2(carol); + multi_action::finalize_and_cage(carol, 2); // check the account is multi_action assert!(multi_action::is_multi_action(carol_address), 7357002); @@ -374,7 +374,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(carol, authorities, option::none()); multi_action::claim_offer(alice, carol_address); multi_action::claim_offer(bob, carol_address); - multi_action::finalize_and_cage2(carol); + multi_action::finalize_and_cage(carol, 2); // propose offer to multisig account multi_action::propose_offer(carol, vector::singleton(dave_address), option::none()); @@ -533,7 +533,7 @@ module ol_framework::test_multi_action { #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] fun finalize_without_gov(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 1); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); } // Try to finalize account without offer @@ -542,7 +542,7 @@ module ol_framework::test_multi_action { fun finalize_without_offer(root: &signer, alice: &signer) { let _vals = mock::genesis_n_vals(root, 1); multi_action::init_gov(alice); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); } // Try to finalize account without enough offer claimed @@ -563,7 +563,7 @@ module ol_framework::test_multi_action { multi_action::claim_offer(bob, alice_address); // finalize the multi_action account - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); } // Try to finalize account already finalized @@ -585,8 +585,8 @@ module ol_framework::test_multi_action { multi_action::claim_offer(carol, alice_address); // finalize the multi_action account - multi_action::finalize_and_cage2(alice); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); + multi_action::finalize_and_cage(alice, 2); } // Governance Tests @@ -610,7 +610,7 @@ module ol_framework::test_multi_action { multi_action::claim_offer(carol, alice_address); // alice finalize multi action workflow to release control of the account - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // bob create a proposal let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); @@ -621,6 +621,17 @@ module ol_framework::test_multi_action { assert!(vector::length(&v) == 0, 7357001); } + // Try to propose an action to a non multisig account + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] + #[expected_failure(abort_code = 0x30006, location = ol_framework::multi_action)] + fun propose_action_to_non_multisig(root: &signer, alice: &signer) { + let _vals = mock::genesis_n_vals(root, 1); + + // alice try to create a proposal to bob account + let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); + let _id = multi_action::propose_new(alice, @0x1000b, proposal); + } + // Multisign authorities bob and carol try to send the same proposal #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] fun propose_action_prevent_duplicated(root: &signer, carol: &signer, alice: &signer, bob: &signer) { @@ -640,7 +651,7 @@ module ol_framework::test_multi_action { multi_action::claim_offer(carol, alice_address); // alice finalize multi action workflow to release control of the account - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); let count = multi_action::get_count_of_pending(alice_address); assert!(count == 0, 7357001); @@ -688,7 +699,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::none()); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // bob create a proposal let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); @@ -726,7 +737,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::none()); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // bob create a proposal and vote let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); @@ -785,7 +796,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(&erik, authorities, option::none()); multi_action::claim_offer(alice, erik_address); multi_action::claim_offer(bob, erik_address); - multi_action::finalize_and_cage2(&erik); + multi_action::finalize_and_cage(&erik, 2); // make a proposal for governance, expires in 2 epoch from now let id = multi_action::propose_governance(alice, erik_address, vector::empty(), true, option::some(1), option::some(2)); @@ -825,7 +836,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(carol, authorities, option::none()); multi_action::claim_offer(alice, carol_address); multi_action::claim_offer(bob, carol_address); - multi_action::finalize_and_cage2(carol); + multi_action::finalize_and_cage(carol, 2); // alice is going to propose to change the authorities to add dave let id = multi_action::propose_governance(alice, carol_address, @@ -906,7 +917,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(&resource_sig, authorities, option::none()); multi_action::claim_offer(carol, new_resource_address); multi_action::claim_offer(bob, new_resource_address); - multi_action::finalize_and_cage2(&resource_sig); + multi_action::finalize_and_cage(&resource_sig, 2); // carol is going to propose to change the authorities to add Rando let id = multi_action::propose_governance(carol, new_resource_address, vector::empty(), true, option::some(1), option::none()); @@ -953,7 +964,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add dave let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), true, option::none(), option::none()); @@ -1017,7 +1028,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(carol, authorities, option::none()); multi_action::claim_offer(alice, carol_address); multi_action::claim_offer(bob, carol_address); - multi_action::finalize_and_cage2(carol); + multi_action::finalize_and_cage(carol, 2); // alice is going to propose to change the authorities to add dave let id = multi_action::propose_governance(alice, carol_address, @@ -1047,7 +1058,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add dave let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0xCAFE), true, option::none(), option::none()); @@ -1071,7 +1082,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add dave twice let authorities = vector::singleton(@0x1000d); @@ -1094,7 +1105,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add dave twice let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(alice_address), true, option::none(), option::none()); @@ -1115,7 +1126,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add bob let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(signer::address_of(bob)), true, option::none(), option::none()); @@ -1136,7 +1147,7 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); - multi_action::finalize_and_cage2(alice); + multi_action::finalize_and_cage(alice, 2); // carol is going to propose to remove dave let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), false, option::none(), option::none()); diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/safe.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/safe.test.move index 0f9ccedca..81350c7a6 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/safe.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/safe.test.move @@ -32,8 +32,15 @@ module ol_framework::test_safe { safe::init_payment_multisig(&resource_sig); // all need to sign + // offer authorities to the safe + multi_action::propose_offer(&resource_sig, vals, option::none()); + + // vals claim the offer + multi_action::claim_offer(alice, new_resource_address); + multi_action::claim_offer(bob, new_resource_address); + //need to be caged to finalize multi action workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, vector::length(&vals)); + multi_action::finalize_and_cage(&resource_sig, vector::length(&vals)); // first make sure dave is initialized to receive LibraCoin ol_account::create_account(root, @0x1000d); @@ -47,8 +54,8 @@ module ol_framework::test_safe { assert!(total_dave == 42, 2); } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, eve = @0x1000e)] - fun propose_payment_should_fail(root: &signer, alice: &signer, bob: &signer, eve: &signer) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, eve = @0x1000e)] + fun propose_payment_should_fail(root: &signer, alice: &signer, bob: &signer, carol: &signer, eve: &signer) { use ol_framework::ol_account; let vals = mock::genesis_n_vals(root, 4); @@ -65,8 +72,16 @@ module ol_framework::test_safe { // not enough voters safe::init_payment_multisig(&resource_sig); // requires 3 + // offer authorities to the safe + multi_action::propose_offer(&resource_sig, vals, option::none()); + + // vals claim the offer + multi_action::claim_offer(alice, new_resource_address); + multi_action::claim_offer(bob, new_resource_address); + multi_action::claim_offer(carol, new_resource_address); + //need to be caged to finalize multi action workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 3); + multi_action::finalize_and_cage(&resource_sig, 3); // first make sure EVE is initialized to receive LibraCoin ol_account::create_account(root, @0x1000e); @@ -80,8 +95,8 @@ module ol_framework::test_safe { assert!(total_dave == 0, 2); } - #[test(root = @ol_framework, alice = @0x1000a, dave = @0x1000d )] - fun safe_root_security_fee(root: &signer, alice: &signer, dave: &signer, ) { + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d )] + fun safe_root_security_fee(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { let vals = mock::genesis_n_vals(root, 3); mock::ol_initialize_coin_and_fund_vals(root, 1000000000000, true); @@ -89,10 +104,19 @@ module ol_framework::test_safe { let (resource_sig, _cap) = ol_account::test_ol_create_resource_account(dave, b"0x1"); let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 0); + safe::init_payment_multisig(&resource_sig); // requires 3 + + // offer authorities to the safe + multi_action::propose_offer(&resource_sig, vals, option::none()); + + // vals claim the offer + multi_action::claim_offer(alice, new_resource_address); + multi_action::claim_offer(bob, new_resource_address); + multi_action::claim_offer(carol, new_resource_address); //need to be caged to finalize multi action workflow and release control of the account - multi_action::finalize_and_cage(&resource_sig, vals, 2); + multi_action::finalize_and_cage(&resource_sig, 2); // fund the account ol_account::transfer(alice, new_resource_address, 1000000); diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/donor_voice_txs.move b/framework/libra-framework/sources/ol_sources/vote_lib/donor_voice_txs.move index 21ea16886..c4210c637 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/donor_voice_txs.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/donor_voice_txs.move @@ -184,7 +184,7 @@ module ol_framework::donor_voice_txs { /// The account must be "bricked" by the owner before MultiSig actions can be taken. /// Note, as with any multisig, the new_authorities cannot include the sponsor, since that account will no longer be able to sign transactions. fun make_multi_action(sponsor: &signer) { - multi_action::init_gov(sponsor,); + multi_action::init_gov(sponsor); multi_action::init_type(sponsor, true); // "true": We make this multisig instance hold the WithdrawCapability. Even though we don't need it for any account pay functions, we can use it to make sure the entire pipeline of private functions scheduling a payment are authorized. Belt and suspenders. } @@ -677,10 +677,11 @@ module ol_framework::donor_voice_txs { } #[test_only] - public fun test_helper_make_donor_voice(vm: &signer, sig: &signer) { - use ol_framework::testnet; - testnet::assert_testnet(vm); - make_donor_voice(sig); + public fun test_helper_make_donor_voice(vm: &signer, sig: &signer, initial_authorities: vector
) { + use ol_framework::testnet; + testnet::assert_testnet(vm); + make_donor_voice(sig); + multi_action::propose_offer(sig, initial_authorities, option::none()); } #[view] diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 93fc98c00..541aa7723 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -32,6 +32,7 @@ module ol_framework::multi_action { use diem_framework::multisig_account; use ol_framework::ballot::{Self, BallotTracker}; use ol_framework::epoch_helper; + use ol_framework::community_wallet; // use diem_std::debug::print; @@ -101,13 +102,15 @@ module ol_framework::multi_action { const EALREADY_OWNER: u64 = 26; /// Owner not found const EOWNER_NOT_FOUND: u64 = 27; + /// Community wallet account + const ECW_ACCOUNT: u64 = 28; /// default setting for a proposal to expire const DEFAULT_EPOCHS_EXPIRE: u64 = 14; /// default setting for an offer to expire const DEFAULT_EPOCHS_OFFER_EXPIRE: u64 = 7; /// minimum number of claimed authorities to cage the account - const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 2; + const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 1; /// maximum number of address to offer const MAX_OFFER_ADDRESSES: u64 = 10; @@ -333,6 +336,16 @@ module ol_framework::multi_action { public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { // Propose the offer on the signer's account let addr = signer::address_of(sig); + + // Ensure the account is not community wallet + // Community wallet has its own propose_offer function + assert!(community_wallet::is_init(addr) == false, error::invalid_argument(ECW_ACCOUNT)); + + propose_offer_internal(sig, proposed, duration_epochs); + } + + public(friend) fun propose_offer_internal(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + let addr = signer::address_of(sig); ensure_valid_propose_offer_state(addr); ensure_valid_propose_offer_params(addr, proposed, duration_epochs); update_offer(addr, proposed, duration_epochs); @@ -379,9 +392,10 @@ module ol_framework::multi_action { /// Finalizes the multisign account and locks it (cage). /// - sig: The signer finalizing the account. + /// - num_signers: The number of signers required to approve a transaction. /// Aborts if governance is not initialized, the account is already a multisig, /// there are not enough claimed authorities, or the offer is not found. - public fun finalize_and_cage2(sig: &signer) acquires Offer { + public fun finalize_and_cage(sig: &signer, num_signers: u64) acquires Offer { let addr = signer::address_of(sig); // check it is not yet initialized @@ -397,7 +411,7 @@ module ol_framework::multi_action { // finalize the account let initial_authorities = get_offer_claimed(addr); - multisig_account::migrate_with_owners(sig, initial_authorities, vector::length(&initial_authorities), vector::empty(), vector::empty()); + multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); // clean offer clean_offer(addr); @@ -429,7 +443,7 @@ module ol_framework::multi_action { } // TODO: remove this function after dependencies are updated - public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { + /*public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { let addr = signer::address_of(sig); assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); @@ -440,7 +454,7 @@ module ol_framework::multi_action { error::invalid_argument(EGOV_NOT_INITIALIZED)); multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); - } + }*/ //////// Helper functions to check initialization ////////// @@ -461,9 +475,9 @@ module ol_framework::multi_action { /// helper to assert if the account is in the right state fun assert_multi_action(addr: address) { - assert!(multisig_account::is_multisig(addr), error::invalid_argument(ENOT_FINALIZED_NOT_BRICK)); - assert!(exists(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); - assert!(exists>(addr), error::invalid_argument(EGOV_NOT_INITIALIZED)); + assert!(multisig_account::is_multisig(addr), error::invalid_state(ENOT_FINALIZED_NOT_BRICK)); + assert!(exists(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); + assert!(exists>(addr), error::invalid_state(EGOV_NOT_INITIALIZED)); } // Query if an offer exists for the given multisig address. @@ -591,22 +605,6 @@ module ol_framework::multi_action { id } - - fun vote_with_data(sig: &signer, proposal: &Proposal, multisig_address: address): (bool, Option) acquires Governance, Action { - assert_authorized(sig, multisig_address); - - let action = borrow_global_mut>(multisig_address); - - // does this proposal already exist in the pending list? - let (found, uid, _idx, _status_enum, _is_complete) = search_proposals_by_data(&action.vote, proposal); - - assert!(found, error::invalid_argument(EPROPOSAL_NOT_FOUND)); - - vote_impl(sig, multisig_address, &uid) - - } - - /// helper function to vote with ID only public(friend) fun vote_with_id(sig: &signer, id: &guid::ID, multisig_address: address): (bool, Option) acquires Governance, Action { assert_authorized(sig, multisig_address); @@ -1011,4 +1009,4 @@ module ol_framework::multi_action { multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); } -} +} \ No newline at end of file From c697c0846fb01ddfee0d8c55c983a5795281dcac Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:01:42 -0300 Subject: [PATCH 34/68] adds propose_offer call to init_payment_multisig --- .../libra-framework/sources/ol_sources/safe.move | 11 +++-------- .../tests/vote_lib/multi_action.test.move | 7 +++---- .../ol_sources/tests/vote_lib/safe.test.move | 15 +++------------ 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/safe.move b/framework/libra-framework/sources/ol_sources/safe.move index bc4f72403..c3f4ff580 100644 --- a/framework/libra-framework/sources/ol_sources/safe.move +++ b/framework/libra-framework/sources/ol_sources/safe.move @@ -48,7 +48,6 @@ module ol_framework::safe { use std::guid; use std::error; use diem_framework::account::WithdrawCapability; - // use diem_framework::coin; use ol_framework::ol_account; use ol_framework::libra_coin; use ol_framework::multi_action; @@ -69,7 +68,6 @@ module ol_framework::safe { const STARTING_FEE: u64 = 00000027; // 1% per year, 0.0027% per epoch const PERCENT_SCALE: u64 = 1000000; // for 4 decimal precision percentages - /// This is the data structure which is stored in the Action for the multisig. struct PaymentType has key, store, copy, drop { // The transaction to be executed @@ -80,17 +78,16 @@ module ol_framework::safe { note: vector, } - /// This fucntion initiates governance for the multisig. It is called by the sponsor address, and is only callable once. + /// This function initiates governance for the multisig. It is called by the sponsor address, and is only callable once. /// init_gov fails gracefully if the governance is already initialized. /// init_type will throw errors if the type is already initialized. - - public entry fun init_payment_multisig(sponsor: &signer) acquires RootMultiSigRegistry { + public entry fun init_payment_multisig(sponsor: &signer, authorities: vector
) acquires RootMultiSigRegistry { multi_action::init_gov(sponsor); multi_action::init_type(sponsor, true); add_to_registry(signer::address_of(sponsor)); + multi_action::propose_offer(sponsor, authorities, option::none()); } - // Propose a transaction // Transactions should be easy, and have one obvious way to do it. There should be no other method for voting for a tx. // this function will catch a duplicate, and vote in its favor. @@ -98,8 +95,6 @@ module ol_framework::safe { // It's optional to state how many epochs from today the transaction should expire. If the transaction is not approved by then, it will be rejected. // The default will be 14 days. // Only the first proposer can set the expiration time. It will be ignored when a duplicate is caught. - - public(friend) fun propose_payment(sig: &signer, multisig_addr: address, recipient: address, amount: u64, note: vector, duration_epochs: Option): guid::ID acquires RootMultiSigRegistry { assert!(is_in_registry(multisig_addr), error::invalid_state(ESAFE_NOT_INITIALIZED)); let pay = new_payment(recipient, amount, *¬e); diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 936417344..ff8ca5fa6 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -788,12 +788,11 @@ module ol_framework::test_multi_action { // fund the account ol_account::transfer(alice, erik_address, 100); + // offer alice and bob authority on the safe - safe::init_payment_multisig(&erik); // both need to sign - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); + let authorities = vector::singleton(signer::address_of(alice)); vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(&erik, authorities, option::none()); + safe::init_payment_multisig(&erik, authorities); // both need to sign multi_action::claim_offer(alice, erik_address); multi_action::claim_offer(bob, erik_address); multi_action::finalize_and_cage(&erik, 2); diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/safe.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/safe.test.move index 81350c7a6..e6c4aaedc 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/safe.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/safe.test.move @@ -30,10 +30,7 @@ module ol_framework::test_safe { // make the vals the signers on the safe // SO ALICE and DAVE ARE AUTHORIZED - safe::init_payment_multisig(&resource_sig); // all need to sign - - // offer authorities to the safe - multi_action::propose_offer(&resource_sig, vals, option::none()); + safe::init_payment_multisig(&resource_sig, vals); // vals claim the offer multi_action::claim_offer(alice, new_resource_address); @@ -70,10 +67,7 @@ module ol_framework::test_safe { // make the vals the signers on the safe // SO ALICE, BOB, CAROL, DAVE ARE AUTHORIZED // not enough voters - safe::init_payment_multisig(&resource_sig); // requires 3 - - // offer authorities to the safe - multi_action::propose_offer(&resource_sig, vals, option::none()); + safe::init_payment_multisig(&resource_sig, vals); // requires 3 // vals claim the offer multi_action::claim_offer(alice, new_resource_address); @@ -105,11 +99,8 @@ module ol_framework::test_safe { let new_resource_address = signer::address_of(&resource_sig); assert!(resource_account::is_resource_account(new_resource_address), 0); - safe::init_payment_multisig(&resource_sig); // requires 3 + safe::init_payment_multisig(&resource_sig, vals); // requires 3 - // offer authorities to the safe - multi_action::propose_offer(&resource_sig, vals, option::none()); - // vals claim the offer multi_action::claim_offer(alice, new_resource_address); multi_action::claim_offer(bob, new_resource_address); From 9981fca702f647e6b28a279b7aeec9cb2ae3093b Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:42:34 -0300 Subject: [PATCH 35/68] fix scenarios --- .../sources/multisig_account.move | 41 ++++++++++++++++++- .../ol_sources/vote_lib/multi_action.move | 2 +- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/framework/libra-framework/sources/multisig_account.move b/framework/libra-framework/sources/multisig_account.move index c4675e7ec..dddc2ba4c 100644 --- a/framework/libra-framework/sources/multisig_account.move +++ b/framework/libra-framework/sources/multisig_account.move @@ -1108,6 +1108,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1168,6 +1170,8 @@ module diem_framework::multisig_account { setup(); let owner_1_addr = address_of(owner_1); create_account(owner_1_addr); + create_account(address_of(owner_2)); + create_account(address_of(owner_3)); create_with_owners(owner_1, vector[address_of(owner_2), address_of(owner_3)], 3, vector[], vector[]); let multisig_account = get_next_multisig_account_address(owner_1_addr); assert_multisig_account_exists(multisig_account); @@ -1197,6 +1201,8 @@ module diem_framework::multisig_account { owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount { setup(); create_account(address_of(owner_1)); + create_account(address_of(owner_2)); + create_account(address_of(owner_3)); create_with_owners( owner_1, vector[ @@ -1224,6 +1230,8 @@ module diem_framework::multisig_account { owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount { setup(); create_account(address_of(owner_1)); + create_account(address_of(owner_2)); + create_account(address_of(owner_3)); create_with_owners(owner_1, vector[ // Duplicate owner 1 addresses. address_of(owner_1), @@ -1244,6 +1252,9 @@ module diem_framework::multisig_account { let auth_key = multi_ed25519::unvalidated_public_key_to_authentication_key(&pk_unvalidated); let multisig_address = from_bcs::to_address(auth_key); create_account(multisig_address); + create_account(@0x123); + create_account(@0x124); + create_account(@0x125); let expected_owners = vector[@0x123, @0x124, @0x125]; let proof = MultisigAccountCreationMessage { @@ -1274,6 +1285,8 @@ module diem_framework::multisig_account { setup(); let owner_1_addr = address_of(owner_1); create_account(owner_1_addr); + create_account(address_of(owner_2)); + create_account(address_of(owner_3)); create_with_owners(owner_1, vector[address_of(owner_2), address_of(owner_3)], 1, vector[], vector[]); let multisig_account = get_next_multisig_account_address(owner_1_addr); assert!(num_signatures_required(multisig_account) == 1, 0); @@ -1329,6 +1342,8 @@ module diem_framework::multisig_account { owner_1: &signer, owner_2: &signer, owner_3: &signer) acquires MultisigAccount { setup(); create_account(address_of(owner_1)); + create_account(address_of(owner_2)); + create_account(address_of(owner_3)); create(owner_1, 1, vector[], vector[]); let owner_1_addr = address_of(owner_1); let owner_2_addr = address_of(owner_2); @@ -1351,6 +1366,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 1, vector[], vector[]); let multisig_account = get_next_multisig_account_address(owner_1_addr); let multisig_signer = &create_signer(multisig_account); @@ -1377,6 +1394,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 1, vector[], vector[]); let multisig_account = get_next_multisig_account_address(owner_1_addr); assert!(owners(multisig_account) == vector[owner_2_addr, owner_3_addr, owner_1_addr], 0); @@ -1393,6 +1412,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); let multisig_account = get_next_multisig_account_address(owner_1_addr); let multisig_signer = &create_signer(multisig_account); @@ -1408,6 +1429,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1485,6 +1508,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1506,6 +1531,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1571,6 +1598,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1632,6 +1661,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1651,6 +1682,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1670,6 +1703,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1689,6 +1724,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); @@ -1707,7 +1744,7 @@ module diem_framework::multisig_account { setup(); create_account(address_of(owner)); let multisig_account = get_next_multisig_account_address(address_of(owner)); - create(owner,1, vector[], vector[]); + create(owner, 1, vector[], vector[]); create_transaction(owner, multisig_account, PAYLOAD); reject_transaction(owner, multisig_account, 1); @@ -1723,6 +1760,8 @@ module diem_framework::multisig_account { let owner_2_addr = address_of(owner_2); let owner_3_addr = address_of(owner_3); create_account(owner_1_addr); + create_account(owner_2_addr); + create_account(owner_3_addr); let multisig_account = get_next_multisig_account_address(owner_1_addr); create_with_owners(owner_1, vector[owner_2_addr, owner_3_addr], 2, vector[], vector[]); diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 541aa7723..eadde3ab6 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -110,7 +110,7 @@ module ol_framework::multi_action { /// default setting for an offer to expire const DEFAULT_EPOCHS_OFFER_EXPIRE: u64 = 7; /// minimum number of claimed authorities to cage the account - const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 1; + const MIN_OFFER_CLAIMS_TO_CAGE: u64 = 2; /// maximum number of address to offer const MAX_OFFER_ADDRESSES: u64 = 10; From c74f148fd039d7b4f7a93d77d6fa6018ece729a6 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:43:31 -0300 Subject: [PATCH 36/68] removes multi_action deprecated finaliza_and_cage version --- .../sources/ol_sources/vote_lib/multi_action.move | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index eadde3ab6..1ad8df9ae 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -442,20 +442,6 @@ module ol_framework::multi_action { assert!(is_authority(multisig_address, sender_addr), error::invalid_argument(ENOT_AUTHORIZED)); } - // TODO: remove this function after dependencies are updated - /*public entry fun finalize_and_cage(sig: &signer, initial_authorities: vector
, num_signers: u64) { - let addr = signer::address_of(sig); - assert!(exists(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - assert!(exists>(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - // not yet initialized - assert!(!multisig_account::is_multisig(addr), - error::invalid_argument(EGOV_NOT_INITIALIZED)); - - multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); - }*/ - //////// Helper functions to check initialization ////////// #[view] From bc249c16b1e83ad3b65373b507b0196af87111b1 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:59:22 -0300 Subject: [PATCH 37/68] break down the update_offer function into 4 smaller functions --- .../ol_sources/vote_lib/multi_action.move | 137 ++++++++++-------- 1 file changed, 78 insertions(+), 59 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 1ad8df9ae..db1f6b1b5 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -221,19 +221,6 @@ module ol_framework::multi_action { init_offer(sig, multisig_address); } - // Private function to assist governance vote - fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { - let offer = borrow_global_mut(addr); - let duration = epoch_helper::get_current_epoch() + DEFAULT_EPOCHS_OFFER_EXPIRE; - let i = 0; - while (i < vector::length(&proposed)) { - let addr = vector::borrow(&proposed, i); - vector::push_back(&mut offer.proposed, *addr); - vector::push_back(&mut offer.expiration_epoch, duration); - i = i + 1; - }; - } - fun ensure_valid_propose_offer_state(addr: address) { // Ensure the account is not yet initialized as multisig assert!(!multisig_account::is_multisig(addr), error::invalid_state(EALREADY_MULTISIG)); @@ -260,7 +247,50 @@ module ol_framework::multi_action { }; } - // Calculate the expiration epoch for the offer. + // Propose an offer to new authorities on the signer account + // or update the expiration epoch of the existing proposed authorities. + // - sig: The signer proposing the offer. + // - proposed: The list of authorities addresses proposed. + // - duration_epochs: The duration in epochs before the offer expires. + public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + // Propose the offer on the signer's account + let addr = signer::address_of(sig); + + // Ensure the account is not community wallet + // Community wallet has its own propose_offer function + assert!(community_wallet::is_init(addr) == false, error::invalid_argument(ECW_ACCOUNT)); + + propose_offer_internal(sig, proposed, duration_epochs); + } + + public(friend) fun propose_offer_internal(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + let addr = signer::address_of(sig); + ensure_valid_propose_offer_state(addr); + ensure_valid_propose_offer_params(addr, proposed, duration_epochs); + update_offer(addr, &mut proposed, duration_epochs); + } + + // Update the offer with the new proposed authorities and expiration epoch. + fun update_offer(addr: address, proposed: &mut vector
, duration_epochs: Option) acquires Offer { + let offer = borrow_global_mut(addr); + + // step 0 + let expiration_epoch = calculate_expiration_epoch(duration_epochs); + + // step 1 + remove_claimed_not_in_new_proposed(offer, proposed); + + // step 2 + remove_new_proposed_addresses_already_claimed(offer, proposed); + + // step 3 + remove_old_proposed_not_in_new_proposed(offer, proposed); + + // step 4 + upsert_new_proposed_and_expiration_epoch(offer, proposed, expiration_epoch); + } + + // update_offer: step 0 fun calculate_expiration_epoch(duration_epochs: Option): u64 { let duration_epochs = if (option::is_some(&duration_epochs)) { *option::borrow(&duration_epochs) @@ -271,84 +301,73 @@ module ol_framework::multi_action { epoch_helper::get_current_epoch() + duration_epochs } - // Update the offer with the new proposed authorities and expiration epoch. - fun update_offer(addr: address, proposed: vector
, duration_epochs: Option) acquires Offer { - let expiration_epoch = calculate_expiration_epoch(duration_epochs); - - // Update offer - let offer = borrow_global_mut(addr); - - // Remove claimed addresses that are not in the new proposed list + // update_offer: step 1 + fun remove_claimed_not_in_new_proposed(offer: &mut Offer, proposed: &vector
) { let j = 0; while (j < vector::length(&offer.claimed)) { let claimed_addr = vector::borrow(&offer.claimed, j); - if (!vector::contains(&proposed, claimed_addr)) { + if (!vector::contains(proposed, claimed_addr)) { vector::remove(&mut offer.claimed, j); } else { j = j + 1; }; }; + } - // Remove new proposed addresses that are already claimed + // update_offer: step 2 + fun remove_new_proposed_addresses_already_claimed(offer: &mut Offer, proposed: &mut vector
) { let i = 0; - while (i < vector::length(&proposed)) { - let proposed_addr = vector::borrow(&proposed, i); + while (i < vector::length(proposed)) { + let proposed_addr = vector::borrow(proposed, i); if (vector::contains(&offer.claimed, proposed_addr)) { - vector::remove(&mut proposed, i); + vector::remove(proposed, i); }; i = i + 1; }; + } - // Remove old proposed addresses that are not in the new proposed list + // update_offer: step 3 + fun remove_old_proposed_not_in_new_proposed(offer: &mut Offer, proposed: &vector
) { let j = 0; while (j < vector::length(&offer.proposed)) { let proposed_addr = vector::borrow(&offer.proposed, j); - if (!vector::contains(&proposed, proposed_addr)) { + if (!vector::contains(proposed, proposed_addr)) { vector::remove(&mut offer.proposed, j); vector::remove(&mut offer.expiration_epoch, j); } else { j = j + 1; }; }; + } - // Insert/Update proposed and expiration epoch lists - let k = 0; - while (k < vector::length(&proposed)) { - // if already contains the address, update the expiration_epoch - let proposed_addr = vector::borrow(&proposed, k); - let (found, i) = vector::index_of(&offer.proposed, proposed_addr); + // update_offer: step 4 + fun upsert_new_proposed_and_expiration_epoch(offer: &mut Offer, proposed: &vector
, expiration_epoch: u64) { + let i = 0; + while (i < vector::length(proposed)) { + let proposed_addr = vector::borrow(proposed, i); + let (found, j) = vector::index_of(&offer.proposed, proposed_addr); if (found) { - vector::remove(&mut offer.expiration_epoch, i); - vector::insert(&mut offer.expiration_epoch, i, expiration_epoch); + vector::remove(&mut offer.expiration_epoch, j); + vector::insert(&mut offer.expiration_epoch, j, expiration_epoch); } else { vector::push_back(&mut offer.proposed, *proposed_addr); vector::push_back(&mut offer.expiration_epoch, expiration_epoch); }; - k = k + 1; + i = i + 1; }; } - // Propose an offer to new authorities on the signer account - // or update the expiration epoch of the existing proposed authorities. - // - sig: The signer proposing the offer. - // - proposed: The list of authorities addresses proposed. - // - duration_epochs: The duration in epochs before the offer expires. - public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { - // Propose the offer on the signer's account - let addr = signer::address_of(sig); - - // Ensure the account is not community wallet - // Community wallet has its own propose_offer function - assert!(community_wallet::is_init(addr) == false, error::invalid_argument(ECW_ACCOUNT)); - - propose_offer_internal(sig, proposed, duration_epochs); - } - - public(friend) fun propose_offer_internal(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { - let addr = signer::address_of(sig); - ensure_valid_propose_offer_state(addr); - ensure_valid_propose_offer_params(addr, proposed, duration_epochs); - update_offer(addr, proposed, duration_epochs); + // Private function to assist governance vote + fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { + let offer = borrow_global_mut(addr); + let duration = epoch_helper::get_current_epoch() + DEFAULT_EPOCHS_OFFER_EXPIRE; + let i = 0; + while (i < vector::length(&proposed)) { + let addr = vector::borrow(&proposed, i); + vector::push_back(&mut offer.proposed, *addr); + vector::push_back(&mut offer.expiration_epoch, duration); + i = i + 1; + }; } // Allows a proposed authority to claim their offer. From e62e07ae982be7bbd4c844ffbdfa58488f633337 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:28:47 -0300 Subject: [PATCH 38/68] propagate n_of_m voted to change after last offer claim --- .../tests/vote_lib/multi_action.test.move | 61 ++++++++++-- .../ol_sources/vote_lib/multi_action.move | 94 +++++++++++++------ 2 files changed, 116 insertions(+), 39 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index ff8ca5fa6..80fad526c 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -2,6 +2,7 @@ module ol_framework::test_multi_action { use ol_framework::mock; use ol_framework::multi_action; + use ol_framework::multisig_account; use ol_framework::safe; use std::signer; use std::option; @@ -837,10 +838,9 @@ module ol_framework::test_multi_action { multi_action::claim_offer(bob, carol_address); multi_action::finalize_and_cage(carol, 2); - // alice is going to propose to change the authorities to add dave + // alice is going to propose to change the authorities to add dave and increase the threshold to 3 let id = multi_action::propose_governance(alice, carol_address, - vector::singleton(dave_address), true, option::none(), - option::none()); + vector::singleton(dave_address), true, option::none(), option::none()); // check authorities did not change let ret = multi_action::get_authorities(carol_address); @@ -855,8 +855,8 @@ module ol_framework::test_multi_action { assert!(ret == authorities, 7357003); // check the Offer - let ret = multi_action::get_offer_proposed(carol_address); - assert!(ret == vector::singleton(dave_address), 7357004); + assert!(multi_action::get_offer_proposed(carol_address) == vector::singleton(dave_address), 7357004); + assert!(multi_action::get_offer_proposed_n_of_m(carol_address) == option::none(), 7357005); // dave claims the offer and it becomes final. multi_action::claim_offer(dave, carol_address); @@ -866,6 +866,10 @@ module ol_framework::test_multi_action { vector::push_back(&mut authorities, dave_address); assert!(ret == authorities, 7357005); + // Check new signitures threshold + assert!(multisig_account::num_signatures_required(carol_address) == 2, 7357006); + assert!(multi_action::get_offer_proposed_n_of_m(carol_address) == option::none(), 7357005); + // Check if offer was cleaned assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357006); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357007); @@ -893,10 +897,12 @@ module ol_framework::test_multi_action { // Happy day: change the threshold of a multisig #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] fun governance_change_threshold(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { - // Scenario: The multisig gets initiated with the 2 bob and carol as the only authorities. It takes 2-of-2 to sign. + // Scenario: The multisig gets initiated with the 2 bob and carol as the only authorities. + // It takes 2-of-2 to sign. // They decide next only 1-of-2 will be needed. + // Then they decide to invite dave and make it 3-of-3. - let _vals = mock::genesis_n_vals(root, 3); + let _vals = mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); // Dave creates the resource account. He is not one of the validators, and is not an authority in the multisig. @@ -918,7 +924,7 @@ module ol_framework::test_multi_action { multi_action::claim_offer(bob, new_resource_address); multi_action::finalize_and_cage(&resource_sig, 2); - // carol is going to propose to change the authorities to add Rando + // carol is going to propose to change the threshold to 1 let id = multi_action::propose_governance(carol, new_resource_address, vector::empty(), true, option::some(1), option::none()); // check authorities and threshold @@ -934,8 +940,9 @@ module ol_framework::test_multi_action { assert!(passed, 7357004); let a = multi_action::get_authorities(new_resource_address); assert!(vector::length(&a) == 2, 7357005); // no change - let (n, _m) = multi_action::get_threshold(new_resource_address); + let (n, m) = multi_action::get_threshold(new_resource_address); assert!(n == 1, 7357006); + assert!(m == 2, 7357006); // now any other type of action can be taken with just one signer let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); @@ -947,6 +954,42 @@ module ol_framework::test_multi_action { assert!(option::is_none(&cap_opt), 7357008); option::destroy_none(cap_opt); + + // now bob decide to invite dave and make it 3-of-3. + multi_action::propose_governance(bob, new_resource_address, vector::singleton(signer::address_of(dave)), true, option::some(3), option::none()); + + // check authorities and threshold did not change + let a = multi_action::get_authorities(new_resource_address); + assert!(vector::length(&a) == 2, 7357010); + assert!(vector::contains(&a, &signer::address_of(bob)), 7357010); + assert!(vector::contains(&a, &signer::address_of(carol)), 7357010); + let (n, m) = multi_action::get_threshold(new_resource_address); + assert!(n == 1, 7357011); + assert!(m == 2, 7357011); + + // check the Offer + assert!(multi_action::get_offer_proposed(new_resource_address) == vector::singleton(signer::address_of(dave)), 7357012); + assert!(multi_action::get_offer_proposed_n_of_m(new_resource_address) == option::some(3), 7357013); + + // dave claims the offer and it becomes final. + multi_action::claim_offer(dave, new_resource_address); + + // Chek new set of authorities + let ret = multi_action::get_authorities(new_resource_address); + vector::push_back(&mut authorities, signer::address_of(dave)); + assert!(ret == vector[ @0x1000c, @0x1000b, @0x1000d ], 7357014); + + // Check new threshold + let (n, m) = multi_action::get_threshold(new_resource_address); + assert!(n == 3, 7357015); + assert!(m == 3, 7357015); + + // Check if offer was cleaned + assert!(multi_action::get_offer_proposed(new_resource_address) == vector::empty(), 7357016); + assert!(multi_action::get_offer_claimed(new_resource_address) == vector::empty(), 7357017); + assert!(multi_action::get_offer_expiration_epoch(new_resource_address) == vector::empty(), 7357018); + assert!(multi_action::get_offer_proposed_n_of_m(new_resource_address) == option::none(), 7357019); + } // Vote new athority before the previous one is claimed diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index db1f6b1b5..6591ab3d7 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -11,16 +11,18 @@ // This is a type of multisig that can be programmable by other on-chain contracts. Previously in V6 we used to call this MultiSig. However platform vendor introduced a new (and great) multisig feature. Both of these can coexist as they have different purposes. +// The module MultiAction allows the sponsor to propose authorities for a future multisig account, which must be claimed by each designated authority. Once enough claims are received, the sponsor can proceed to set up the multisig account. After the account becomes multisig, any changes to authorities must be voted on. + // With vendor::MultiSig, the action needs to be constructed offline using a script. There are advantages to this. Anything that can be written into a script can be made to execute by the authorities. However the code is not inspectable, it's stored as bytecode. MultiAction, on the other hand requires that all the logic be written in a deployed contract. The execution of the action depends on the third-party published contract. MultiAction will only return the Capabilities necessary for the smart contract to do what it needs, e.g. a simple passed/rejected result, an optional WithdrawCapability, and and optional (dangerous) SignerCapability (tbd as of V7). -// similarly any handler for the Action can be executed by an external contract, and the Governance module will only check if the Action has been approved by the required number of authorities. +// Similarly any handler for the Action can be executed by an external contract, and the Governance module will only check if the Action has been approved by the required number of authorities. // Each Action has a separate data structure for tabulating the votes in approval of the Action. But there is shared state between the Actions, that being Governance, which contains the constraints for each Action that are checked on each vote (n_sigs, expiration, signers, etc) // The Actions are triggered "lazily", that is: the last authorized sender of a proposal/vote, is the one to trigger the action. // Theere is no offline signature aggregation. The authorities over the address should not require collecting signatures offline: proposal should be submitted directly to this contract. // With this design, the multisig can be used for different actions. The safe.move contract is an example of a Root Service which the chain provides, which leverages the Governance module to provide a payment service which requires n-of-m approvals. -//V7 NOTE: from V6 we are refactoring so the the account first needs to be created as a "resource account". It's a minor change given that V6 had a similar construct of a "signerless account", Previously in ol this meant to "Brick" the authkey after the WithdrawCapability was stored in a common struct. Vendor had independenly made the same design using Signer Capability. +// V7 NOTE: from V6 we are refactoring so the the account first needs to be created as a "resource account". It's a minor change given that V6 had a similar construct of a "signerless account", Previously in 0L this meant to "Brick" the authkey after the WithdrawCapability was stored in a common struct. Vendor had independenly made the same design using Signer Capability. module ol_framework::multi_action { use std::vector; @@ -28,8 +30,8 @@ module ol_framework::multi_action { use std::signer; use std::error; use std::guid; - use diem_framework::account::{Self, WithdrawCapability}; use diem_framework::multisig_account; + use diem_framework::account::{Self, WithdrawCapability}; use ol_framework::ballot::{Self, BallotTracker}; use ol_framework::epoch_helper; use ol_framework::community_wallet; @@ -165,10 +167,12 @@ module ol_framework::multi_action { /// - proposed: List of authority addresses proposed /// - claimed: List of authority addresses that have claimed the offer. /// - expiration_epoch: The epoch when each proposed expires. + /// - proposed_n_of_m: The n-of-m threshold for the account. Used only after account is cage. struct Offer has key, store { proposed: vector
, claimed: vector
, expiration_epoch: vector, + proposed_n_of_m: Option, } fun construct_empty_offer(): Offer { @@ -176,6 +180,7 @@ module ol_framework::multi_action { proposed: vector::empty(), claimed: vector::empty(), expiration_epoch: vector::empty(), + proposed_n_of_m: option::none(), } } @@ -184,6 +189,7 @@ module ol_framework::multi_action { offer.proposed = vector::empty(); offer.claimed = vector::empty(); offer.expiration_epoch = vector::empty(); + offer.proposed_n_of_m = option::none(); } public(friend) fun init_offer(sig: &signer, addr: address) { @@ -357,39 +363,16 @@ module ol_framework::multi_action { }; } - // Private function to assist governance vote - fun add_offer_addresses(addr: address, proposed: vector
) acquires Offer { - let offer = borrow_global_mut(addr); - let duration = epoch_helper::get_current_epoch() + DEFAULT_EPOCHS_OFFER_EXPIRE; - let i = 0; - while (i < vector::length(&proposed)) { - let addr = vector::borrow(&proposed, i); - vector::push_back(&mut offer.proposed, *addr); - vector::push_back(&mut offer.expiration_epoch, duration); - i = i + 1; - }; - } - // Allows a proposed authority to claim their offer. // - sig: The signer making the claim. // - multisig_address: The address of the multisig account. public fun claim_offer(sig: &signer, multisig_address: address) acquires Offer, Governance { let sender_addr = signer::address_of(sig); - // Ensure the account has an offer - assert!(exists_offer(multisig_address), error::not_found(ENOT_OFFERED)); - - // Ensure the offer has not expired - assert!(!is_offer_expired(multisig_address, sender_addr), error::out_of_range(EOFFER_EXPIRED)); + validate_claim_offer(multisig_address, sender_addr); let offer = borrow_global_mut(multisig_address); - // Ensure the sender is not in the claimed list - assert!(!vector::contains(&offer.claimed, &sender_addr), error::already_exists(EALREADY_CLAIMED)); - - // Ensure the sender is in the proposed list - assert!(vector::contains(&offer.proposed, &sender_addr), error::not_found(EADDRESS_NOT_PROPOSED)); - // Remove the sender from the proposed list and expiration_epoch let (_, i) = vector::index_of(&offer.proposed, &sender_addr); vector::remove(&mut offer.proposed, i); @@ -400,6 +383,10 @@ module ol_framework::multi_action { let ms = borrow_global_mut(multisig_address); maybe_update_authorities(ms, true, &vector::singleton(sender_addr)); if (vector::length(&offer.proposed) == 0) { + // Update voted n_of_m after all authorities claimed + let gov = borrow_global_mut(multisig_address); + maybe_update_threshold(gov, &offer.proposed_n_of_m); + // clean the Offer clean_offer(multisig_address); }; @@ -409,6 +396,23 @@ module ol_framework::multi_action { }; } + // Validate account state and parameters to claim the offer. + fun validate_claim_offer(multisig_address: address, sender_addr: address) acquires Offer{ + // Ensure the account has an offer + assert!(exists_offer(multisig_address), error::not_found(ENOT_OFFERED)); + + // Ensure the offer has not expired + assert!(!is_offer_expired(multisig_address, sender_addr), error::out_of_range(EOFFER_EXPIRED)); + + let offer = borrow_global(multisig_address); + + // Ensure the sender is not in the claimed list + assert!(!vector::contains(&offer.claimed, &sender_addr), error::already_exists(EALREADY_CLAIMED)); + + // Ensure the sender is in the proposed list + assert!(vector::contains(&offer.proposed, &sender_addr), error::not_found(EADDRESS_NOT_PROPOSED)); + } + /// Finalizes the multisign account and locks it (cage). /// - sig: The signer finalizing the account. /// - num_signers: The number of signers required to approve a transaction. @@ -506,6 +510,10 @@ module ol_framework::multi_action { borrow_global(multisig_address).expiration_epoch } + public fun get_offer_proposed_n_of_m(multisig_address: address): Option acquires Offer { + borrow_global(multisig_address).proposed_n_of_m + } + // Query if the offer has enough claimed authorities to cage the account. fun has_enough_offer_claimed(multisig_address: address): bool acquires Offer { let claimed = get_offer_claimed(multisig_address); @@ -879,8 +887,7 @@ module ol_framework::multi_action { let data = extract_proposal_data(multisig_address, id); if (!vector::is_empty(&data.addresses)) { if (data.add_remove) { - // offer the authority adition voted to be claimed - add_offer_addresses(multisig_address, data.addresses); + propose_voted_offer(multisig_address, data.addresses, &data.n_of_m); return passed } else { maybe_update_authorities(ms, data.add_remove, &data.addresses); @@ -891,6 +898,33 @@ module ol_framework::multi_action { passed } + // New authorities voted must claim the offer to become authorities. + fun propose_voted_offer(multisig_address: address, new_authorities: vector
, n_of_m: &Option) acquires Offer { + let offer = borrow_global_mut(multisig_address); + let duration = epoch_helper::get_current_epoch() + DEFAULT_EPOCHS_OFFER_EXPIRE; + let i = 0; + while (i < vector::length(&new_authorities)) { + let addr = vector::borrow(&new_authorities, i); + vector::push_back(&mut offer.proposed, *addr); + vector::push_back(&mut offer.expiration_epoch, duration); + i = i + 1; + }; + maybe_update_threshold_after_claim(multisig_address, n_of_m); + } + + // If authorities voted to change the number of signatures required along authorities addition, + // new authorities must claim the offer before the number of signatures required is applied. + fun maybe_update_threshold_after_claim(multisig_address: address, n_of_m: &Option) acquires Offer { + if (option::is_some(n_of_m)) { + let new_n_of_m = *option::borrow(n_of_m); + let current_n_of_m = multisig_account::num_signatures_required(multisig_address); + if (current_n_of_m != new_n_of_m) { + let offer = borrow_global_mut(multisig_address); + offer.proposed_n_of_m = option::some(new_n_of_m); + }; + }; + } + /// Updates the authorities of the multisig. This is a helper function for governance. // must be called with the withdraw capability and signer. belt and suspenders fun maybe_update_authorities(ms: &mut Governance, add_remove: bool, addresses: &vector
) { @@ -900,7 +934,7 @@ module ol_framework::multi_action { fun maybe_update_threshold(ms: &mut Governance, n_of_m_opt: &Option) { if (option::is_some(n_of_m_opt)) { - multisig_account::multi_auth_helper_update_signatures_required(&ms.guid_capability, *option::borrow(n_of_m_opt)); + multisig_account::multi_auth_helper_update_signatures_required(&ms.guid_capability, *option::borrow(n_of_m_opt)); }; } From 228ff500936bb1e67bbf3f5dbf439a52c8116146 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Wed, 19 Jun 2024 11:49:04 -0300 Subject: [PATCH 39/68] add test two_simultaneous_governance_vote + clean up code --- .../tests/vote_lib/multi_action.test.move | 551 ++++++++---------- 1 file changed, 230 insertions(+), 321 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 80fad526c..5aeb810f7 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -14,7 +14,7 @@ module ol_framework::test_multi_action { use diem_framework::account; // print - use std::debug::print; + // use std::debug::print; struct DummyType has drop, store {} @@ -33,8 +33,8 @@ module ol_framework::test_multi_action { // Happy Day: propose offer to authorities #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] - fun propose_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 4); + fun propose_offer(root: &signer, carol: &signer) { + mock::genesis_n_vals(root, 4); let carol_address = @0x1000c; // check the offer does not exist @@ -47,37 +47,31 @@ module ol_framework::test_multi_action { assert!(multi_action::exists_offer(carol_address), 7357004); // offer authorities - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b], option::none()); // check the offer is proposed and account is not muti_action yet assert!(multi_action::exists_offer(carol_address), 7357005); - assert!(multi_action::get_offer_proposed(carol_address) == authorities, 7357006); + assert!(multi_action::get_offer_proposed(carol_address) == vector[@0x1000a, @0x1000b], 7357006); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357007); assert!(vector::is_empty(&multi_action::get_offer_claimed(carol_address)), 7357008); - let expiration = vector::empty(); - vector::push_back(&mut expiration, 7); - vector::push_back(&mut expiration, 7); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == expiration, 7357009); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector[7, 7], 7357009); assert!(!multi_action::is_multi_action(carol_address), 7357010); } // Propose new offer after expired #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] fun propose_offer_after_expired(root: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; // initialize the multi_action account multi_action::init_gov(carol); // offer to alice - multi_action::propose_offer(carol, vector::singleton(@0x1000a), option::some(2)); + multi_action::propose_offer(carol, vector[@0x1000a], option::some(2)); // check the offer is valid - assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::singleton(2), 7357004); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector[2], 7357004); // wait for the offer to expire mock::trigger_epoch(root); // epoch 1 valid @@ -85,36 +79,33 @@ module ol_framework::test_multi_action { assert!(multi_action::is_offer_expired(carol_address, @0x1000a), 7357005); // propose a new offer to bob - multi_action::propose_offer(carol, vector::singleton(@0x1000b), option::some(3)); + multi_action::propose_offer(carol, vector[@0x1000b], option::some(3)); // check the new offer is proposed - assert!(multi_action::get_offer_proposed(carol_address) == vector::singleton(@0x1000b), 7357007); + assert!(multi_action::get_offer_proposed(carol_address) == vector[@0x1000b], 7357007); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357008); - assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::singleton(5), 7357009); + assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector[5], 7357009); } // Happy Day: claim offer by authorities #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] fun claim_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 2); + mock::genesis_n_vals(root, 2); let carol_address = @0x1000c; // initialize the multi_action account multi_action::init_gov(carol); // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b], option::none()); // bob claim the offer multi_action::claim_offer(bob, carol_address); // check the claimed offer assert!(multi_action::exists_offer(carol_address), 7357001); - let claimed = vector::singleton(signer::address_of(bob)); - let proposed = vector::singleton(signer::address_of(alice)); + let claimed = vector[signer::address_of(bob)]; + let proposed = vector[signer::address_of(alice)]; assert!(multi_action::get_offer_claimed(carol_address) == claimed, 7357002); assert!(multi_action::get_offer_proposed(carol_address) == proposed, 7357003); @@ -123,27 +114,21 @@ module ol_framework::test_multi_action { // check alice and bob claimed the offer let claimed = multi_action::get_offer_claimed(carol_address); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(alice)); - assert!(claimed == authorities, 7357004); + assert!(claimed == vector[@0x1000b, @0x1000a], 7357004); assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357005); } // Happy Day: finalize multisign account #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] fun finalize_multi_action(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; // initialize the multi_action account multi_action::init_gov(carol); // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b], option::none()); // authorities claim the offer multi_action::claim_offer(alice, carol_address); @@ -158,10 +143,7 @@ module ol_framework::test_multi_action { // check authorities let authorities = multi_action::get_authorities(carol_address); - let claimed = vector::empty
(); - vector::push_back(&mut claimed, signer::address_of(alice)); - vector::push_back(&mut claimed, signer::address_of(bob)); - assert!(authorities == claimed, 7357003); + assert!(authorities == vector[@0x1000a, @0x1000b], 7357003); // check offer was cleaned assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); @@ -171,19 +153,15 @@ module ol_framework::test_multi_action { // Finalize multisign account having a pending claim #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b, dave = @0x1000d)] - fun finalize_with_pending_claim(root: &signer, carol: &signer, alice: &signer, bob: &signer, dave: &signer) { - let _vals = mock::genesis_n_vals(root, 4); + fun finalize_with_pending_claim(root: &signer, carol: &signer, alice: &signer, bob: &signer) { + mock::genesis_n_vals(root, 4); let carol_address = @0x1000c; // initialize the multi_action account multi_action::init_gov(carol); // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(dave)); - multi_action::propose_offer(carol, authorities, option::none()); + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b, @0x1000d], option::none()); // authorities claim the offer multi_action::claim_offer(alice, carol_address); @@ -198,133 +176,104 @@ module ol_framework::test_multi_action { // check authorities let authorities = multi_action::get_authorities(carol_address); - let claimed = vector::empty
(); - vector::push_back(&mut claimed, signer::address_of(alice)); - vector::push_back(&mut claimed, signer::address_of(bob)); - assert!(authorities == claimed, 7357003); + assert!(authorities == vector[@0x1000a, @0x1000b], 7357003); // check offer was cleared assert!(multi_action::get_offer_proposed(carol_address) == vector::empty(), 7357004); assert!(multi_action::get_offer_claimed(carol_address) == vector::empty(), 7357005); assert!(multi_action::get_offer_expiration_epoch(carol_address) == vector::empty(), 7357006); - } // Propose another offer with different authorities #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] fun propose_another_offer_different_authorities(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); multi_action::init_gov(alice); // invite bob - multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(1)); + multi_action::propose_offer(alice, vector[@0x1000b], option::some(1)); // invite carol and dave - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x1000c); - vector::push_back(&mut authorities, @0x1000d); - multi_action::propose_offer(alice, authorities, option::some(2)); + multi_action::propose_offer(alice, vector[@0x1000c, @0x1000d], option::some(2)); // check new authorities - let expiration = vector::singleton(2); - vector::push_back(&mut expiration, 2); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357001); - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357002); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector[2, 2], 7357001); + assert!(multi_action::get_offer_proposed(@0x1000a) == vector[@0x1000c, @0x1000d], 7357002); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357003); } // Propose new offer with more authorities #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] fun propose_offer_more_authorities(root: &signer, alice: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); multi_action::init_gov(alice); // invite bob - multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(1)); + multi_action::propose_offer(alice, vector[@0x1000b], option::some(1)); // new invite bob and carol - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x1000b); - vector::push_back(&mut authorities, @0x1000c); - multi_action::propose_offer(alice, authorities, option::some(2)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::some(2)); // check offer - assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); + assert!(multi_action::get_offer_proposed(@0x1000a) == vector[@0x1000b, @0x1000c], 7357001); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); - let expiration = vector::singleton(2); - vector::push_back(&mut expiration, 2); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector[2, 2], 7357003); // carol claim the offer multi_action::claim_offer(carol, @0x1000a); // new invite bob, carol and dave - vector::push_back(&mut authorities, @0x1000d); - multi_action::propose_offer(alice, authorities, option::some(3)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c, @0x1000d], option::some(3)); // check new authorities - let proposed = vector::singleton(@0x1000b); - vector::push_back(&mut proposed, @0x1000d); - let expiration = vector::singleton(3); - vector::push_back(&mut expiration, 3); - assert!(multi_action::get_offer_proposed(@0x1000a) == proposed, 7357004); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000c), 7357005); + assert!(multi_action::get_offer_proposed(@0x1000a) == vector[@0x1000b, @0x1000d], 7357004); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector[3, 3], 7357003); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector[@0x1000c], 7357005); } // Propose new offer with less authorities #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] fun propose_offer_less_authorities(root: &signer, alice: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); multi_action::init_gov(alice); // invite bob, carol e dave - let authorities = vector::singleton(@0x1000b); - vector::push_back(&mut authorities, @0x1000c); - vector::push_back(&mut authorities, @0x1000d); - multi_action::propose_offer(alice, authorities, option::some(2)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c, @0x1000d], option::some(2)); // new invite bob e carol - let new_authorities = vector::singleton(@0x1000b); - vector::push_back(&mut new_authorities, @0x1000c); + let new_authorities = vector[@0x1000b, @0x1000c]; multi_action::propose_offer(alice, new_authorities, option::some(3)); // check new authorities minus dave assert!(multi_action::get_offer_proposed(@0x1000a) == new_authorities, 7357001); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); - let expiration = vector::singleton(3); - vector::push_back(&mut expiration, 3); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector[3, 3], 7357003); // carol claim the offer multi_action::claim_offer(carol, @0x1000a); // new invite bob only - multi_action::propose_offer(alice, vector::singleton(@0x1000b), option::some(4)); + multi_action::propose_offer(alice, vector[@0x1000b], option::some(4)); // check new authorities minus carol - assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000b), 7357003); + assert!(multi_action::get_offer_proposed(@0x1000a) == vector[@0x1000b], 7357003); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357004); } // Propose new offer with same authorities #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] fun propose_offer_same_authorities(root: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); // invite bob e carol - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x1000b); - vector::push_back(&mut authorities, @0x1000c); + let authorities = vector[@0x1000b, @0x1000c]; multi_action::propose_offer(alice, authorities, option::some(2)); // check offer assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); - let expiration = vector::singleton(2); - vector::push_back(&mut expiration, 2); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector[2, 2], 7357003); // new invite bob e carol multi_action::propose_offer(alice, authorities, option::some(3)); @@ -332,9 +281,7 @@ module ol_framework::test_multi_action { // check offer assert!(multi_action::get_offer_proposed(@0x1000a) == authorities, 7357001); assert!(multi_action::get_offer_claimed(@0x1000a) == vector::empty(), 7357002); - let expiration = vector::singleton(3); - vector::push_back(&mut expiration, 3); - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == expiration, 7357003); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector[3, 3], 7357003); // bob claim the offer multi_action::claim_offer(bob, @0x1000a); @@ -343,49 +290,43 @@ module ol_framework::test_multi_action { multi_action::propose_offer(alice, authorities, option::some(4)); // check authorities - assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector::singleton(4), 7357002); - assert!(multi_action::get_offer_proposed(@0x1000a) == vector::singleton(@0x1000c), 7357003); - assert!(multi_action::get_offer_claimed(@0x1000a) == vector::singleton(@0x1000b), 7357004); + assert!(multi_action::get_offer_expiration_epoch(@0x1000a) == vector[4], 7357002); + assert!(multi_action::get_offer_proposed(@0x1000a) == vector[@0x1000c], 7357003); + assert!(multi_action::get_offer_claimed(@0x1000a) == vector[@0x1000b], 7357004); } // Try to propose offer without governance #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] - fun propose_offer_without_gov(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + fun propose_offer_without_gov(root: &signer, carol: &signer) { + mock::genesis_n_vals(root, 3); // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b], option::none()); } // Try to propose offer to an multisig account #[test(root = @ol_framework, dave = @0x1000d, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x30013, location = ol_framework::multi_action)] fun propose_offer_to_multisign(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); let carol_address = @0x1000c; let dave_address = @0x1000d; multi_action::init_gov(carol); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b], option::none()); multi_action::claim_offer(alice, carol_address); multi_action::claim_offer(bob, carol_address); multi_action::finalize_and_cage(carol, 2); // propose offer to multisig account - multi_action::propose_offer(carol, vector::singleton(dave_address), option::none()); + multi_action::propose_offer(carol, vector[dave_address], option::none()); } // Try to propose an empty offer #[test(root = @ol_framework, alice = @0x1000a)] #[expected_failure(abort_code = 0x10010, location = ol_framework::multi_action)] fun propose_empty_offer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); multi_action::init_gov(alice); multi_action::propose_offer(alice, vector::empty
(), option::none()); } @@ -394,20 +335,9 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a)] #[expected_failure(abort_code = 0x10018, location = ol_framework::multi_action)] fun propose_too_many_authorities(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); + mock::genesis_n_vals(root, 1); multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, @0x10001); - vector::push_back(&mut authorities, @0x10002); - vector::push_back(&mut authorities, @0x10003); - vector::push_back(&mut authorities, @0x10004); - vector::push_back(&mut authorities, @0x10005); - vector::push_back(&mut authorities, @0x10006); - vector::push_back(&mut authorities, @0x10007); - vector::push_back(&mut authorities, @0x10008); - vector::push_back(&mut authorities, @0x10009); - vector::push_back(&mut authorities, @0x10010); - vector::push_back(&mut authorities, @0x10011); + let authorities = vector[@0x10001, @0x10002, @0x10003, @0x10004, @0x10005, @0x10006, @0x10007, @0x10008, @0x10009, @0x10010, @0x10011]; multi_action::propose_offer(alice, authorities, option::none()); } @@ -415,7 +345,7 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a)] #[expected_failure(abort_code = 0x1000D, location = ol_framework::multisig_account)] fun propose_offer_to_signer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); + mock::genesis_n_vals(root, 1); let alice_address = signer::address_of(alice); multi_action::init_gov(alice); multi_action::propose_offer(alice, vector::singleton
(alice_address), option::none()); @@ -425,52 +355,42 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 0x10001, location = ol_framework::multisig_account)] fun propose_offer_duplicated_authorities(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); - - let authorities = vector::singleton
(@0x1000b); - vector::push_back(&mut authorities, @0x1000c); - vector::push_back(&mut authorities, @0x1000b); - multi_action::propose_offer(alice, authorities, option::none()); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c, @0x1000b], option::none()); } // Try to propose offer to an invalid signer #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x60012, location = ol_framework::multisig_account)] fun offer_to_invalid_authority(root: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); // propose to invalid address - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, @0xCAFE); + let authorities = vector[signer::address_of(bob), @0xCAFE]; multi_action::propose_offer(alice, authorities, option::some(2)); } // Try to propose offer with zero duration epochs #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x10016, location = ol_framework::multi_action)] - fun offer_with_zero_duration(root: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + fun offer_with_zero_duration(root: &signer, alice: &signer) { + mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); - - // propose to invalid address - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(alice, authorities, option::some(0)); + multi_action::propose_offer(alice, vector[@0x1000b], option::some(0)); } // Try to claim offer not offered to signer #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 0x60014, location = ol_framework::multi_action)] fun claim_offer_not_offered(root: &signer, alice: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; multi_action::init_gov(carol); // invite bob - multi_action::propose_offer(carol, vector::singleton(@0x1000b), option::none()); + multi_action::propose_offer(carol, vector[@0x1000b], option::none()); // alice try to claim the offer multi_action::claim_offer(alice, carol_address); @@ -480,49 +400,39 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x2000F, location = ol_framework::multi_action)] fun claim_expired_offer(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let carol_address = @0x1000c; + mock::genesis_n_vals(root, 3); multi_action::init_gov(carol); - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::some(2)); + // offer to alice and bob + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b], option::some(2)); // alice claim the offer - multi_action::claim_offer(alice, carol_address); + multi_action::claim_offer(alice, @0x1000c); mock::trigger_epoch(root); // epoch 1 valid mock::trigger_epoch(root); // epoch 2 valid mock::trigger_epoch(root); // epoch 3 expired // bob claim expired offer - multi_action::claim_offer(bob, carol_address); + multi_action::claim_offer(bob, @0x1000c); } // Try to claim offer of an account without proposal #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x60011, location = ol_framework::multi_action)] fun claim_offer_without_proposal(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 2); - let bob_address = @0x1000c; - multi_action::claim_offer(alice, bob_address); + mock::genesis_n_vals(root, 2); + multi_action::claim_offer(alice, @0x1000c); } // Try to claim offer twice #[test(root = @ol_framework, carol = @0x1000c, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x80017, location = ol_framework::multi_action)] - fun claim_offer_twice(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + fun claim_offer_twice(root: &signer, carol: &signer, alice: &signer) { + mock::genesis_n_vals(root, 3); let carol_address = @0x1000c; multi_action::init_gov(carol); - - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::some(2)); + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b], option::some(2)); // Alice claim the offer twice multi_action::claim_offer(alice, carol_address); @@ -533,7 +443,7 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a)] #[expected_failure(abort_code = 0x30001, location = ol_framework::multi_action)] fun finalize_without_gov(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); + mock::genesis_n_vals(root, 1); multi_action::finalize_and_cage(alice, 2); } @@ -541,7 +451,7 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a)] #[expected_failure(abort_code = 0x30012, location = ol_framework::multi_action)] fun finalize_without_offer(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); + mock::genesis_n_vals(root, 1); multi_action::init_gov(alice); multi_action::finalize_and_cage(alice, 2); } @@ -549,19 +459,15 @@ module ol_framework::test_multi_action { // Try to finalize account without enough offer claimed #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 0x30012, location = ol_framework::multi_action)] - fun finalize_without_enough_claimed(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); - let alice_address = @0x1000a; + fun finalize_without_enough_claimed(root: &signer, alice: &signer, bob: &signer) { + mock::genesis_n_vals(root, 3); multi_action::init_gov(alice); - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); + // offer to bob and carol authority on the alice account + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::none()); // bob claim the offer - multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(bob, @0x1000a); // finalize the multi_action account multi_action::finalize_and_cage(alice, 2); @@ -571,15 +477,12 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 0x80013, location = ol_framework::multi_action)] fun finalize_already_finalized(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; multi_action::init_gov(alice); - // invite the vals to the resource account - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); + // offer bob and carol authority on the alice account + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::none()); // bob claim the offer multi_action::claim_offer(bob, alice_address); @@ -595,16 +498,13 @@ module ol_framework::test_multi_action { // Happy Day: propose a new action and check zero votes #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] fun propose_action(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; // offer to bob and carol authority on the alice safe multi_action::init_gov(alice); multi_action::init_type(alice, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::none()); // bob and alice claim the offer multi_action::claim_offer(bob, alice_address); @@ -626,7 +526,7 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] #[expected_failure(abort_code = 0x30006, location = ol_framework::multi_action)] fun propose_action_to_non_multisig(root: &signer, alice: &signer) { - let _vals = mock::genesis_n_vals(root, 1); + mock::genesis_n_vals(root, 1); // alice try to create a proposal to bob account let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); @@ -636,16 +536,13 @@ module ol_framework::test_multi_action { // Multisign authorities bob and carol try to send the same proposal #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] fun propose_action_prevent_duplicated(root: &signer, carol: &signer, alice: &signer, bob: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; // offer to bob and carol authority on the alice safe multi_action::init_gov(alice); multi_action::init_type(alice, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::none()); // bob and alice claim the offer multi_action::claim_offer(bob, alice_address); @@ -689,15 +586,12 @@ module ol_framework::test_multi_action { // Scenario: a simple MultiAction where we don't need any capabilities. Only need to know if the result was successful on the vote that crossed the threshold. // transform alice account in multisign with bob and carol as authorities - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; multi_action::init_gov(alice); // Ths is a simple multi_action: there is no capability being stored multi_action::init_type(alice, false); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::none()); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); multi_action::finalize_and_cage(alice, 2); @@ -722,7 +616,7 @@ module ol_framework::test_multi_action { fun vote_action_happy_withdraw_cap(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // Scenario: testing that a payment type multisig could be created with this module: that the WithdrawCapability can be used here. - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); let alice_address = @0x1000a; @@ -732,10 +626,7 @@ module ol_framework::test_multi_action { // make the bob and carol the signers on the alice safe, and 2-of-2 need to sign multi_action::init_gov(alice); multi_action::init_type(alice, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::none()); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::none()); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); multi_action::finalize_and_cage(alice, 2); @@ -754,10 +645,7 @@ module ol_framework::test_multi_action { // THE WITHDRAW CAPABILITY IS WHERE WE EXPECT assert!(option::is_some(&cap_opt), 7357003); let cap = option::extract(&mut cap_opt); - let c = ol_account::withdraw_with_capability( - &cap, - 42, - ); + let c = ol_account::withdraw_with_capability(&cap, 42,); // deposit to erik account ol_account::create_account(root, @0x1000e); ol_account::deposit_coins(@0x1000e, c); @@ -775,7 +663,7 @@ module ol_framework::test_multi_action { fun vote_action_expiration(root: &signer, alice: &signer, bob: &signer, dave: &signer) { // Scenario: Testing that if an action expires voting cannot be done. - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); // we are at epoch 0 @@ -791,15 +679,14 @@ module ol_framework::test_multi_action { ol_account::transfer(alice, erik_address, 100); // offer alice and bob authority on the safe - let authorities = vector::singleton(signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - safe::init_payment_multisig(&erik, authorities); // both need to sign + safe::init_payment_multisig(&erik, vector[@0x1000a, @0x1000b]); // both need to sign multi_action::claim_offer(alice, erik_address); multi_action::claim_offer(bob, erik_address); multi_action::finalize_and_cage(&erik, 2); // make a proposal for governance, expires in 2 epoch from now - let id = multi_action::propose_governance(alice, erik_address, vector::empty(), true, option::some(1), option::some(2)); + let id = multi_action::propose_governance(alice, erik_address, vector::empty(), true, + option::some(1), option::some(2)); mock::trigger_epoch(root); // epoch 1 mock::trigger_epoch(root); // epoch 2 @@ -820,7 +707,7 @@ module ol_framework::test_multi_action { // later they add a third (Dave) so it becomes a 2-of-3. // Dave and Bob, then remove alice so it becomes 2-of-2 again - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); let carol_address = @0x1000c; let dave_address = @0x1000d; @@ -830,9 +717,7 @@ module ol_framework::test_multi_action { // offer alice and bob authority on the safe multi_action::init_gov(carol);// both need to sign multi_action::init_type(carol, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); + let authorities = vector[@0x1000a, @0x1000b]; multi_action::propose_offer(carol, authorities, option::none()); multi_action::claim_offer(alice, carol_address); multi_action::claim_offer(bob, carol_address); @@ -840,7 +725,7 @@ module ol_framework::test_multi_action { // alice is going to propose to change the authorities to add dave and increase the threshold to 3 let id = multi_action::propose_governance(alice, carol_address, - vector::singleton(dave_address), true, option::none(), option::none()); + vector[dave_address], true, option::none(), option::none()); // check authorities did not change let ret = multi_action::get_authorities(carol_address); @@ -855,7 +740,7 @@ module ol_framework::test_multi_action { assert!(ret == authorities, 7357003); // check the Offer - assert!(multi_action::get_offer_proposed(carol_address) == vector::singleton(dave_address), 7357004); + assert!(multi_action::get_offer_proposed(carol_address) == vector[dave_address], 7357004); assert!(multi_action::get_offer_proposed_n_of_m(carol_address) == option::none(), 7357005); // dave claims the offer and it becomes final. @@ -877,15 +762,15 @@ module ol_framework::test_multi_action { // Now dave and bob, will conspire to remove alice. // NOTE: `false` means `remove account` here - let id = multi_action::propose_governance(dave, carol_address, vector::singleton(signer::address_of(alice)), false, option::none(), option::none()); - let a = multi_action::get_authorities(carol_address); - assert!(vector::length(&a) == 3, 7357009); // no change yet + let id = multi_action::propose_governance(dave, carol_address, vector[signer::address_of(alice)], false, option::none(), option::none()); + let authorities = multi_action::get_authorities(carol_address); + assert!(vector::length(&authorities) == 3, 7357009); // no change yet // bob votes and it becomes final. Bob could either use vote_governance() let passed = multi_action::vote_governance(bob, carol_address, &id); assert!(passed, 7357008); - let a = multi_action::get_authorities(carol_address); - assert!(vector::length(&a) == 2, 73570010); + let authorities = multi_action::get_authorities(carol_address); + assert!(vector::length(&authorities) == 2, 73570010); assert!(!multi_action::is_authority(carol_address, signer::address_of(alice)), 7357011); // Check if offer was cleaned @@ -902,7 +787,7 @@ module ol_framework::test_multi_action { // They decide next only 1-of-2 will be needed. // Then they decide to invite dave and make it 3-of-3. - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); // Dave creates the resource account. He is not one of the validators, and is not an authority in the multisig. @@ -916,10 +801,7 @@ module ol_framework::test_multi_action { // offer bob and carol authority on the safe multi_action::init_gov(&resource_sig);// both need to sign multi_action::init_type(&resource_sig, false); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(&resource_sig, authorities, option::none()); + multi_action::propose_offer(&resource_sig, vector[@0x1000b, @0x1000c], option::none()); multi_action::claim_offer(carol, new_resource_address); multi_action::claim_offer(bob, new_resource_address); multi_action::finalize_and_cage(&resource_sig, 2); @@ -928,125 +810,115 @@ module ol_framework::test_multi_action { let id = multi_action::propose_governance(carol, new_resource_address, vector::empty(), true, option::some(1), option::none()); // check authorities and threshold - let a = multi_action::get_authorities(new_resource_address); - assert!(vector::length(&a) == 2, 7357002); // no change - let (n, _m) = multi_action::get_threshold(new_resource_address); + let authorities = multi_action::get_authorities(new_resource_address); + assert!(authorities == vector[@0x1000c, @0x1000b], 7357002); // no change + let (n, m) = multi_action::get_threshold(new_resource_address); assert!(n == 2, 7357003); + assert!(m == 2, 7357004); // bob votes and it becomes final. Bob could either use vote_governance() let passed = multi_action::vote_governance(bob, new_resource_address, &id); // check authorities and threshold - assert!(passed, 7357004); - let a = multi_action::get_authorities(new_resource_address); - assert!(vector::length(&a) == 2, 7357005); // no change + assert!(passed, 7357005); + let authorities = multi_action::get_authorities(new_resource_address); + assert!(authorities == vector[@0x1000c, @0x1000b], 7357006); // no change let (n, m) = multi_action::get_threshold(new_resource_address); - assert!(n == 1, 7357006); - assert!(m == 2, 7357006); + assert!(n == 1, 7357007); + assert!(m == 2, 7357008); // now any other type of action can be taken with just one signer let proposal = multi_action::proposal_constructor(DummyType{}, option::none()); let id = multi_action::propose_new(bob, new_resource_address, proposal); let (passed, cap_opt) = multi_action::vote_with_id(bob, &id, new_resource_address); - assert!(passed == true, 7357007); + assert!(passed == true, 7357009); // THE WITHDRAW CAPABILITY IS MISSING AS EXPECTED - assert!(option::is_none(&cap_opt), 7357008); + assert!(option::is_none(&cap_opt), 7357010); option::destroy_none(cap_opt); // now bob decide to invite dave and make it 3-of-3. - multi_action::propose_governance(bob, new_resource_address, vector::singleton(signer::address_of(dave)), true, option::some(3), option::none()); + multi_action::propose_governance(bob, new_resource_address, vector[signer::address_of(dave)], true, option::some(3), option::none()); // check authorities and threshold did not change - let a = multi_action::get_authorities(new_resource_address); - assert!(vector::length(&a) == 2, 7357010); - assert!(vector::contains(&a, &signer::address_of(bob)), 7357010); - assert!(vector::contains(&a, &signer::address_of(carol)), 7357010); + let authorities = multi_action::get_authorities(new_resource_address); + assert!(vector::length(&authorities) == 2, 7357011); + assert!(vector::contains(&authorities, &signer::address_of(bob)), 7357012); + assert!(vector::contains(&authorities, &signer::address_of(carol)), 7357013); let (n, m) = multi_action::get_threshold(new_resource_address); - assert!(n == 1, 7357011); - assert!(m == 2, 7357011); + assert!(n == 1, 7357014); + assert!(m == 2, 7357015); // check the Offer - assert!(multi_action::get_offer_proposed(new_resource_address) == vector::singleton(signer::address_of(dave)), 7357012); - assert!(multi_action::get_offer_proposed_n_of_m(new_resource_address) == option::some(3), 7357013); + assert!(multi_action::get_offer_proposed(new_resource_address) == vector[signer::address_of(dave)], 7357016); + assert!(multi_action::get_offer_proposed_n_of_m(new_resource_address) == option::some(3), 7357017); // dave claims the offer and it becomes final. multi_action::claim_offer(dave, new_resource_address); // Chek new set of authorities let ret = multi_action::get_authorities(new_resource_address); - vector::push_back(&mut authorities, signer::address_of(dave)); - assert!(ret == vector[ @0x1000c, @0x1000b, @0x1000d ], 7357014); + assert!(ret == vector[ @0x1000c, @0x1000b, @0x1000d ], 7357018); // Check new threshold let (n, m) = multi_action::get_threshold(new_resource_address); - assert!(n == 3, 7357015); - assert!(m == 3, 7357015); + assert!(n == 3, 7357019); + assert!(m == 3, 7357020); // Check if offer was cleaned - assert!(multi_action::get_offer_proposed(new_resource_address) == vector::empty(), 7357016); - assert!(multi_action::get_offer_claimed(new_resource_address) == vector::empty(), 7357017); - assert!(multi_action::get_offer_expiration_epoch(new_resource_address) == vector::empty(), 7357018); - assert!(multi_action::get_offer_proposed_n_of_m(new_resource_address) == option::none(), 7357019); - + assert!(multi_action::get_offer_proposed(new_resource_address) == vector::empty(), 7357021); + assert!(multi_action::get_offer_claimed(new_resource_address) == vector::empty(), 7357022); + assert!(multi_action::get_offer_expiration_epoch(new_resource_address) == vector::empty(), 7357023); + assert!(multi_action::get_offer_proposed_n_of_m(new_resource_address) == option::none(), 7357024); } // Vote new athority before the previous one is claimed #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, erik = @0x1000e)] fun governance_vote_before_claim(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 5); + mock::genesis_n_vals(root, 5); let alice_address = @0x1000a; // alice offer bob and carol authority on her account multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add dave - let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), true, option::none(), option::none()); + let id = multi_action::propose_governance(carol, alice_address, vector[@0x1000d], true, option::none(), option::none()); // bob votes and dave does not claims the offer multi_action::vote_governance(bob, alice_address, &id); // check authorities and threshold - assert!(multi_action::get_authorities(alice_address) == authorities, 7357001); + assert!(multi_action::get_authorities(alice_address) == vector[@0x1000b, @0x1000c], 7357001); let (n, _m) = multi_action::get_threshold(alice_address); assert!(n == 2, 7357002); // check offer - print(&multi_action::get_offer_proposed(alice_address)); assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357003); - print(&multi_action::get_offer_proposed(alice_address)); - assert!(multi_action::get_offer_proposed(alice_address) == vector::singleton(@0x1000d), 7357004); - assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector::singleton(7), 7357005); + assert!(multi_action::get_offer_proposed(alice_address) == vector[@0x1000d], 7357004); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector[7], 7357005); mock::trigger_epoch(root); // epoch 1 // bob is going to propose to change the authorities to add erik - let id = multi_action::propose_governance(bob, alice_address, vector::singleton(@0x1000e), true, option::none(), option::none()); + let id = multi_action::propose_governance(bob, alice_address, vector[@0x1000e], true, option::none(), option::none()); // carol votes multi_action::vote_governance(carol, alice_address, &id); // check authorities and threshold - assert!(multi_action::get_authorities(alice_address) == authorities, 7357001); + assert!(multi_action::get_authorities(alice_address) == vector[@0x1000b, @0x1000c], 7357001); let (n, _m) = multi_action::get_threshold(alice_address); assert!(n == 2, 7357002); // check offer assert!(multi_action::get_offer_claimed(alice_address) == vector::empty(), 7357003); - let proposed = vector::singleton(@0x1000d); - vector::push_back(&mut proposed, @0x1000e); - assert!(multi_action::get_offer_proposed(alice_address) == proposed, 7357004); - let expiration = vector::singleton(7); - vector::push_back(&mut expiration, 8); - assert!(multi_action::get_offer_expiration_epoch(alice_address) == expiration, 7357005); + assert!(multi_action::get_offer_proposed(alice_address) == vector[@0x1000d, @0x1000e], 7357004); + assert!(multi_action::get_offer_expiration_epoch(alice_address) == vector[7, 8], 7357005); } // Try to vote twice on the same ballot @@ -1055,7 +927,7 @@ module ol_framework::test_multi_action { fun vote_action_twice(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // Scenario: Testing that a vote cannot be done twice on the same ballot. - let _vals = mock::genesis_n_vals(root, 4); + mock::genesis_n_vals(root, 4); mock::ol_initialize_coin_and_fund_vals(root, 10000000, true); let carol_address = @0x1000c; let dave_address = @0x1000d; @@ -1064,18 +936,14 @@ module ol_framework::test_multi_action { // offer alice and bob authority on the safe multi_action::init_gov(carol); multi_action::init_type(carol, true); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(alice)); - vector::push_back(&mut authorities, signer::address_of(bob)); - multi_action::propose_offer(carol, authorities, option::none()); + multi_action::propose_offer(carol, vector[@0x1000a, @0x1000b], option::none()); multi_action::claim_offer(alice, carol_address); multi_action::claim_offer(bob, carol_address); multi_action::finalize_and_cage(carol, 2); // alice is going to propose to change the authorities to add dave - let id = multi_action::propose_governance(alice, carol_address, - vector::singleton(dave_address), true, option::none(), - option::none()); + let id = multi_action::propose_governance(alice, carol_address, + vector[dave_address], true, option::none(), option::none()); // bob votes let passed = multi_action::vote_governance(bob, carol_address, &id); @@ -1089,21 +957,18 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 0x60012, location = ol_framework::multisig_account)] fun governance_vote_invalid_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; // alice offer bob and carol authority on her account multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add dave - let id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0xCAFE), true, option::none(), option::none()); + let id = multi_action::propose_governance(carol, alice_address, vector[@0xCAFE], true, option::none(), option::none()); // bob votes and dave does not claims the offer multi_action::vote_governance(bob, alice_address, &id); @@ -1113,85 +978,129 @@ module ol_framework::test_multi_action { #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] #[expected_failure(abort_code = 0x10001, location = ol_framework::multisig_account)] fun governance_vote_duplicated_addresses(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 5); + mock::genesis_n_vals(root, 5); let alice_address = @0x1000a; // alice offer bob and carol authority on her account multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add dave twice - let authorities = vector::singleton(@0x1000d); - vector::push_back(&mut authorities, @0x1000d); - let _id = multi_action::propose_governance(carol, alice_address, authorities, true, option::none(), option::none()); + let _id = multi_action::propose_governance(carol, alice_address, + vector[@0x1000d, @0x1000d], true, option::none(), option::none()); } // Try to vote multisig account address for new authority #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] #[expected_failure(abort_code = 0x1000D, location = ol_framework::multisig_account)] fun governance_vote_multisig_address(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 5); + mock::genesis_n_vals(root, 5); let alice_address = @0x1000a; // alice offer bob and carol authority on her account multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); multi_action::finalize_and_cage(alice, 2); - // carol is going to propose to change the authorities to add dave twice - let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(alice_address), true, option::none(), option::none()); + // carol try to propose to change the authorities adding alice + let _id = multi_action::propose_governance(carol, alice_address, vector[alice_address], true, option::none(), option::none()); } // Try to vote an owner as new authority #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 0x1001A, location = ol_framework::multi_action)] fun governance_vote_owner_as_new_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; // alice offer bob and carol authority on her account multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); multi_action::finalize_and_cage(alice, 2); // carol is going to propose to change the authorities to add bob - let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(signer::address_of(bob)), true, option::none(), option::none()); + let _id = multi_action::propose_governance(carol, alice_address, vector[@0x1000b], true, option::none(), option::none()); } // Try to vote remove an authority that is not in the multisig #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] #[expected_failure(abort_code = 0x6001B, location = ol_framework::multi_action)] fun governance_vote_remove_non_authority(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - let _vals = mock::genesis_n_vals(root, 3); + mock::genesis_n_vals(root, 3); let alice_address = @0x1000a; // alice offer bob and carol authority on her account multi_action::init_gov(alice); - let authorities = vector::empty
(); - vector::push_back(&mut authorities, signer::address_of(bob)); - vector::push_back(&mut authorities, signer::address_of(carol)); - multi_action::propose_offer(alice, authorities, option::some(1)); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::some(1)); multi_action::claim_offer(bob, alice_address); multi_action::claim_offer(carol, alice_address); multi_action::finalize_and_cage(alice, 2); // carol is going to propose to remove dave - let _id = multi_action::propose_governance(carol, alice_address, vector::singleton(@0x1000d), false, option::none(), option::none()); + let _id = multi_action::propose_governance(carol, alice_address, + vector[@0x1000d], false, option::none(), option::none()); + } + + // Two governance proposals at the same time + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, eve = @0x1000e)] + fun two_simultaneous_governance_vote(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer, eve: &signer) { + mock::genesis_n_vals(root, 5); + let alice_address = @0x1000a; + + // alice offer bob and carol authority on her account + multi_action::init_gov(alice); + multi_action::propose_offer(alice, vector[@0x1000b, @0x1000c], option::some(1)); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::finalize_and_cage(alice, 2); + + // carol is going to propose to change the authorities to add dave + let id_set_n_3 = multi_action::propose_governance(carol, alice_address, vector[@0x1000d], true, option::some(3), option::none()); + + // bob is going to propose to change the authorities to add erik + let id_set_n_1 = multi_action::propose_governance(bob, alice_address, vector[@0x1000e], true, option::some(1), option::none()); + + // bob votes + let passed = multi_action::vote_governance(bob, alice_address, &id_set_n_3); + assert!(passed, 7357001); + + // check offer + assert!(multi_action::get_offer_proposed(alice_address) == vector[@0x1000d], 7357002); + assert!(multi_action::get_offer_proposed_n_of_m(alice_address) == option::some(3), 7357003); + + // carol votes + let passed = multi_action::vote_governance(carol, alice_address, &id_set_n_1); + assert!(passed, 7357004); + + // check offer + assert!(multi_action::get_offer_proposed(alice_address) == vector[@0x1000d, @0x1000e], 7357005); + assert!(multi_action::get_offer_proposed_n_of_m(alice_address) == option::some(1), 7357006); + + // dave claims the offer + multi_action::claim_offer(dave, alice_address); + + // check authorities and threshold + let authorities = multi_action::get_authorities(alice_address); + assert!(authorities == vector[@0x1000b, @0x1000c, @0x1000d], 7357007); + let (n, m) = multi_action::get_threshold(alice_address); + assert!(n == 2, 7357008); // no change + assert!(m == 3, 7357009); + + // eve claims the offer and the threshold is now 1 + multi_action::claim_offer(eve, alice_address); + + // check authorities and threshold + let authorities = multi_action::get_authorities(alice_address); + assert!(authorities == vector[@0x1000b, @0x1000c, @0x1000d, @0x1000e], 7357010); + let (n, m) = multi_action::get_threshold(alice_address); + assert!(n == 1, 7357011); + assert!(m == 4, 7357012); } } From 868e9805cb7ab17d2c0c0333427496606e194e32 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Wed, 19 Jun 2024 12:55:48 -0300 Subject: [PATCH 40/68] ensure that offer n_of_m will be cleaned after a change of n_of_m --- .../tests/vote_lib/multi_action.test.move | 6 ++++- .../ol_sources/vote_lib/multi_action.move | 24 ++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move index 5aeb810f7..66e5dd5b6 100644 --- a/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/vote_lib/multi_action.test.move @@ -14,7 +14,7 @@ module ol_framework::test_multi_action { use diem_framework::account; // print - // use std::debug::print; + //use std::debug::print; struct DummyType has drop, store {} @@ -638,6 +638,10 @@ module ol_framework::test_multi_action { assert!(passed == false, 7357001); option::destroy_none(cap_opt); + // query proposal id + let creation_number = multi_action::get_pending_by_creation_number(alice_address); + assert!(creation_number == vector[guid::id_creation_num(&id)], 7357002); + // carol vote on bob proposal let (passed, cap_opt) = multi_action::vote_with_id(carol, &id, alice_address); assert!(passed == true, 7357002); diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 6591ab3d7..7a2225f16 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -380,12 +380,13 @@ module ol_framework::multi_action { if (multisig_account::is_multisig(multisig_address)) { // a) finalized account: add authority to the multisig account - let ms = borrow_global_mut(multisig_address); - maybe_update_authorities(ms, true, &vector::singleton(sender_addr)); + let gov = borrow_global_mut(multisig_address); + maybe_update_authorities(gov, true, &vector::singleton(sender_addr)); if (vector::length(&offer.proposed) == 0) { // Update voted n_of_m after all authorities claimed - let gov = borrow_global_mut(multisig_address); - maybe_update_threshold(gov, &offer.proposed_n_of_m); + let n_of_m = offer.proposed_n_of_m; + let _ = offer; + maybe_update_threshold(multisig_address, gov, &n_of_m); // clean the Offer clean_offer(multisig_address); @@ -883,17 +884,17 @@ module ol_framework::multi_action { maybe_restore_withdraw_cap(cap_opt); // don't need this but can't drop. if (passed) { - let ms = borrow_global_mut(multisig_address); + let governance = borrow_global_mut(multisig_address); let data = extract_proposal_data(multisig_address, id); if (!vector::is_empty(&data.addresses)) { if (data.add_remove) { propose_voted_offer(multisig_address, data.addresses, &data.n_of_m); return passed } else { - maybe_update_authorities(ms, data.add_remove, &data.addresses); + maybe_update_authorities(governance, data.add_remove, &data.addresses); }; }; - maybe_update_threshold(ms, &data.n_of_m); + maybe_update_threshold(multisig_address, governance, &data.n_of_m); }; passed } @@ -932,9 +933,14 @@ module ol_framework::multi_action { multisig_account::multi_auth_helper_add_remove(&ms.guid_capability, add_remove, addresses); } - fun maybe_update_threshold(ms: &mut Governance, n_of_m_opt: &Option) { + fun maybe_update_threshold(multisig_address: address, governance: &mut Governance, n_of_m_opt: &Option) acquires Offer { if (option::is_some(n_of_m_opt)) { - multisig_account::multi_auth_helper_update_signatures_required(&ms.guid_capability, *option::borrow(n_of_m_opt)); + multisig_account::multi_auth_helper_update_signatures_required(&governance.guid_capability, + *option::borrow(n_of_m_opt)); + + // clean the Offer n_of_m to avoid a future claim change the n_of_m + let offer = borrow_global_mut(multisig_address); + offer.proposed_n_of_m = option::none(); }; } From a5220de3aeefb9b0f585a30266487aa07c575114 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:04:56 -0300 Subject: [PATCH 41/68] set multi_action propose_offer not a transaction --- .../sources/ol_sources/safe.move | 4 +- .../ol_sources/vote_lib/donor_voice_txs.move | 2 +- .../ol_sources/vote_lib/multi_action.move | 2 +- framework/releases/head.mrb | Bin 848339 -> 863126 bytes tools/txs/src/txs_cli_community.rs | 217 ++++++++++-------- 5 files changed, 124 insertions(+), 101 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/safe.move b/framework/libra-framework/sources/ol_sources/safe.move index c3f4ff580..cb8314d85 100644 --- a/framework/libra-framework/sources/ol_sources/safe.move +++ b/framework/libra-framework/sources/ol_sources/safe.move @@ -6,7 +6,7 @@ // The main design goals of this multisig implementation are: -// 0 . Leverages MultiSig library which allows for arbitrary transaction types to be handled by the multisig. This is a payments implementation. +// 0. Leverages MultiSig library which allows for arbitrary transaction types to be handled by the multisig. This is a payments implementation. // 1. should leverage the usual transaction flow and tools which users are familiar with to add funds to the account. The funds remain viewable by the usual tools for viewing account balances. // 2. The authority over the address should not require collecting signatures offline: transactions should be submitted directly to the contract. // 3. Funds are disbursed as usual: to a destination addresses, and not into any intermediate structures. @@ -85,7 +85,7 @@ module ol_framework::safe { multi_action::init_gov(sponsor); multi_action::init_type(sponsor, true); add_to_registry(signer::address_of(sponsor)); - multi_action::propose_offer(sponsor, authorities, option::none()); + multi_action::propose_offer_internal(sponsor, authorities, option::none()); } // Propose a transaction diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/donor_voice_txs.move b/framework/libra-framework/sources/ol_sources/vote_lib/donor_voice_txs.move index c4210c637..011290e05 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/donor_voice_txs.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/donor_voice_txs.move @@ -681,7 +681,7 @@ module ol_framework::donor_voice_txs { use ol_framework::testnet; testnet::assert_testnet(vm); make_donor_voice(sig); - multi_action::propose_offer(sig, initial_authorities, option::none()); + multi_action::propose_offer_internal(sig, initial_authorities, option::none()); } #[view] diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 7a2225f16..c06d4eb18 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -258,7 +258,7 @@ module ol_framework::multi_action { // - sig: The signer proposing the offer. // - proposed: The list of authorities addresses proposed. // - duration_epochs: The duration in epochs before the offer expires. - public entry fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { + public fun propose_offer(sig: &signer, proposed: vector
, duration_epochs: Option) acquires Offer { // Propose the offer on the signer's account let addr = signer::address_of(sig); diff --git a/framework/releases/head.mrb b/framework/releases/head.mrb index e70f92e8def28695b57b024c77378ffc2f3fcb4d..670c4b5f21bb64314ebfe0d002f114e640657784 100644 GIT binary patch delta 88273 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%0D}X0J<~(F)dRGH0)fuA&3LR#oA@z%?ZEtl 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 delta 73896 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

4Ity!~Y6zzWP@bJj}z`uEu27KnQ)rRc;n!Q1N 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 R=q$IY|KmN=O?36A{SQ6`->U!s diff --git a/tools/txs/src/txs_cli_community.rs b/tools/txs/src/txs_cli_community.rs index 1e67b9038..35faa70b1 100644 --- a/tools/txs/src/txs_cli_community.rs +++ b/tools/txs/src/txs_cli_community.rs @@ -10,49 +10,71 @@ use std::{collections::HashMap, fs, path::PathBuf}; #[derive(clap::Subcommand)] pub enum CommunityTxs { + /// Initialize a DonorVoice multi-sig by proposing an offer to initial authorities. + /// NOTE: Then authorities need to claim the offer, and the donor needs to cage the account to become a multi-sig account. + GovInit(InitTx), + /// Update proposed offer to initial authorities + /*GovOffer(OfferTx), + /// Claim the proposed offer + GovClaim(ClaimTx), + /// Finalize and cage the multisig account after authorities claim the offer + GovCage(CageTx), + /// Propose a change to the authorities of the DonorVoice multi-sig + */ + GovAdmin(AdminTx), /// Propose a multi-sig transaction Propose(ProposeTx), /// Execute batch proposals/approvals of transactions Batch(BatchTx), /// Donors to Donor Voice addresses can vote to reject transactions Veto(VetoTx), - /// Initialize a DonorVoice multi-sig. NOTE: this is a two step procedure: - /// propose the admins, and then rotate the account keys with --finalize - GovInit(InitTx), - /// Propose a change to the authorities of the DonorVoice multi-sig - GovAdmin(AdminTx), } impl CommunityTxs { pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { match &self { - CommunityTxs::Propose(propose) => match propose.run(sender).await { - Ok(_) => println!("SUCCESS: community wallet transfer proposed"), + CommunityTxs::GovInit(init) => match init.run(sender).await { + Ok(_) => println!("SUCCESS: community wallet initialized"), Err(e) => { - println!("ERROR: community wallet transfer rejected, message: {}", e); + println!("ERROR: could not initialize Community Wallet, message: {}", e); } }, - CommunityTxs::Veto(veto) => match veto.run(sender).await { - Ok(_) => println!("SUCCESS: veto vote submitted"), + /*CommunityTxs::GovOffer(offer) => match offer.run(sender).await { + Ok(_) => println!("SUCCESS: community wallet offer proposed"), Err(e) => { - println!("ERROR: veto vote rejected, message: {}", e); + println!("ERROR: could not propose offer, message: {}", e); } }, - CommunityTxs::GovInit(init) => match init.run(sender).await { - Ok(_) => println!("SUCCESS: community wallet initialized"), + CommunityTxs::GovClaim(claim) => match claim.run(sender).await { + Ok(_) => println!("SUCCESS: community wallet offer claimed"), Err(e) => { - println!( - "ERROR: could not initialize Community Wallet, message: {}", - e - ); + println!("ERROR: could not claim offer, message: {}", e); } }, + CommunityTxs::GovCage(cage) => match cage.run(sender).await { + Ok(_) => println!("SUCCESS: community wallet finalized"), + Err(e) => { + println!("ERROR: could not finalize wallet, message: {}", e); + } + },*/ CommunityTxs::GovAdmin(admin) => match admin.run(sender).await { Ok(_) => println!("SUCCESS: community wallet admin added"), Err(e) => { println!("ERROR: could not add admin, message: {}", e); } }, + CommunityTxs::Propose(propose) => match propose.run(sender).await { + Ok(_) => println!("SUCCESS: community wallet transfer proposed"), + Err(e) => { + println!("ERROR: community wallet transfer rejected, message: {}", e); + } + }, + CommunityTxs::Veto(veto) => match veto.run(sender).await { + Ok(_) => println!("SUCCESS: veto vote submitted"), + Err(e) => { + println!("ERROR: veto vote rejected, message: {}", e); + } + }, CommunityTxs::Batch(batch) => match batch.run(sender).await { Ok(_) => {} Err(e) => { @@ -65,6 +87,87 @@ impl CommunityTxs { } } +#[derive(clap::Args)] +/// Initialize a community wallet in two steps 1) make it a donor voice account, +/// and check proposed authorities 2) finalize and set the authorities +pub struct InitTx { + #[clap(short, long)] + /// The initial admins of the multi-sig (cannot add self) + pub admins: Vec, + + #[clap(short, long)] + /// Num of signatures needed for the n-of-m + pub num_signers: u64, + + #[clap(long)] + /// Finalize the configurations and rotate the auth key, not reversible! + pub finalize: bool, +} + +impl InitTx { + pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { + if self.finalize { + // Warning message + println!("\nWARNING: This operation will finalize the account associated with the governance-initialized wallet and make it inaccessible. This action is IRREVERSIBLE and can only be applied to a wallet where governance has been initialized.\n"); + + // Assuming the signer's account is already set in the `sender` object + // The payload for the finalize and cage operation + let payload = + libra_stdlib::community_wallet_init_finalize_and_cage(self.num_signers); // This function now does not require an account address + + // Execute the transaction + sender.sign_submit_wait(payload).await?; + println!("The account has been finalized and caged."); + } else { + let payload = libra_stdlib::community_wallet_init_init_community( + self.admins.clone(), + self.num_signers, + ); + + sender.sign_submit_wait(payload).await?; + println!("You have completed the first step in creating a community wallet, now you should check your work and finalize with --finalize"); + } + + Ok(()) + } +} + +#[derive(clap::Args)] +pub struct AdminTx { + #[clap(short, long)] + /// The SlowWallet recipient of funds + pub community_wallet: AccountAddress, + #[clap(short, long)] + /// Admin to add (or remove) from the multisig + pub admin: AccountAddress, + #[clap(short, long)] + /// Drops this admin from the multisig + pub drop: Option, + #[clap(short, long)] + /// Number of sigs required for action (must be greater than 3-of-5) + pub n: u64, + #[clap(short, long)] + /// Proposal duration (in epochs) + pub epochs: Option, +} + +impl AdminTx { + pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { + // Default to adding a signer if the `drop` flag is not provided + let is_add_operation = self.drop.unwrap_or(true); + + let payload = libra_stdlib::community_wallet_init_change_signer_community_multisig( + self.community_wallet, + self.admin, + is_add_operation, + self.n, + self.epochs.unwrap_or(10), // todo: remo + ); + sender.sign_submit_wait(payload).await?; + Ok(()) + } +} + #[derive(clap::Args)] pub struct ProposeTx { #[clap(short, long)] @@ -327,83 +430,3 @@ impl VetoTx { } } -#[derive(clap::Args)] -/// Initialize a community wallet in two steps 1) make it a donor voice account, -/// and check proposed authorities 2) finalize and set the authorities -pub struct InitTx { - #[clap(short, long)] - /// The initial admins of the multi-sig (cannot add self) - pub admins: Vec, - - #[clap(short, long)] - /// Num of signatures needed for the n-of-m - pub num_signers: u64, - - #[clap(long)] - /// Finalize the configurations and rotate the auth key, not reversible! - pub finalize: bool, -} - -impl InitTx { - pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { - if self.finalize { - // Warning message - println!("\nWARNING: This operation will finalize the account associated with the governance-initialized wallet and make it inaccessible. This action is IRREVERSIBLE and can only be applied to a wallet where governance has been initialized.\n"); - - // Assuming the signer's account is already set in the `sender` object - // The payload for the finalize and cage operation - let payload = - libra_stdlib::multi_action_finalize_and_cage(self.admins.clone(), self.num_signers); // This function now does not require an account address - - // Execute the transaction - sender.sign_submit_wait(payload).await?; - println!("The account has been finalized and caged."); - } else { - let payload = libra_stdlib::community_wallet_init_init_community( - self.admins.clone(), - self.num_signers, - ); - - sender.sign_submit_wait(payload).await?; - println!("You have completed the first step in creating a community wallet, now you should check your work and finalize with --finalize"); - } - - Ok(()) - } -} - -#[derive(clap::Args)] -pub struct AdminTx { - #[clap(short, long)] - /// The SlowWallet recipient of funds - pub community_wallet: AccountAddress, - #[clap(short, long)] - /// Admin to add (or remove) from the multisig - pub admin: AccountAddress, - #[clap(short, long)] - /// Drops this admin from the multisig - pub drop: Option, - #[clap(short, long)] - /// Number of sigs required for action (must be greater than 3-of-5) - pub n: u64, - #[clap(short, long)] - /// Proposal duration (in epochs) - pub epochs: Option, -} - -impl AdminTx { - pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { - // Default to adding a signer if the `drop` flag is not provided - let is_add_operation = self.drop.unwrap_or(true); - - let payload = libra_stdlib::community_wallet_init_change_signer_community_multisig( - self.community_wallet, - self.admin, - is_add_operation, - self.n, - self.epochs.unwrap_or(10), // todo: remo - ); - sender.sign_submit_wait(payload).await?; - Ok(()) - } -} From 5a8f67373900448f1a8c08d0974392cb7f63a364 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Thu, 20 Jun 2024 21:21:15 -0300 Subject: [PATCH 42/68] implementing first smoke test to create community wallet with offer/claim pattern --- .../src/libra_framework_sdk_builder.rs | 287 +++++++++++------- .../ol_sources/vote_lib/multi_action.move | 25 +- framework/move-stdlib/sources/error.move | 2 +- tools/txs/src/txs_cli_community.rs | 64 ++-- tools/txs/tests/cw_temp.rs | 260 ++++++++++++++++ 5 files changed, 499 insertions(+), 139 deletions(-) create mode 100644 tools/txs/tests/cw_temp.rs diff --git a/framework/cached-packages/src/libra_framework_sdk_builder.rs b/framework/cached-packages/src/libra_framework_sdk_builder.rs index cc4f54078..828b99af1 100644 --- a/framework/cached-packages/src/libra_framework_sdk_builder.rs +++ b/framework/cached-packages/src/libra_framework_sdk_builder.rs @@ -166,15 +166,20 @@ pub enum EntryFunctionCall { /// convenience function to check if the account can be caged /// after all the structs are in place CommunityWalletInitFinalizeAndCage { - initial_authorities: Vec, num_signers: u64, }, CommunityWalletInitInitCommunity { - check_addresses: Vec, + initial_authorities: Vec, check_threshold: u64, }, + /// Propose offer to the multisig, and check if the signers are not related family + CommunityWalletInitProposeOffer { + new_signers: Vec, + num_signers: u64, + }, + DiemGovernanceAddApprovedScriptHashScript { proposal_id: u64, }, @@ -278,10 +283,12 @@ pub enum EntryFunctionCall { amount: u64, }, - /// finalize the account and put in a cage. Will abort if governance has not - MultiActionFinalizeAndCage { - initial_authorities: Vec, - num_signers: u64, + MultiActionClaimOffer { + multisig_address: AccountAddress, + }, + + MultiActionMigrationMigrateOffer { + multisig_address: AccountAddress, }, /// Similar to add_owners, but only allow adding one owner. @@ -462,10 +469,12 @@ pub enum EntryFunctionCall { epoch_expiry: u64, }, - /// This fucntion initiates governance for the multisig. It is called by the sponsor address, and is only callable once. + /// This function initiates governance for the multisig. It is called by the sponsor address, and is only callable once. /// init_gov fails gracefully if the governance is already initialized. /// init_type will throw errors if the type is already initialized. - SafeInitPaymentMultisig {}, + SafeInitPaymentMultisig { + authorities: Vec, + }, SlowWalletSmokeTestVmUnlock { user_addr: AccountAddress, @@ -618,14 +627,17 @@ impl EntryFunctionCall { n_of_m, vote_duration_epochs, ), - CommunityWalletInitFinalizeAndCage { - initial_authorities, - num_signers, - } => community_wallet_init_finalize_and_cage(initial_authorities, num_signers), + CommunityWalletInitFinalizeAndCage { num_signers } => { + community_wallet_init_finalize_and_cage(num_signers) + } CommunityWalletInitInitCommunity { - check_addresses, + initial_authorities, check_threshold, - } => community_wallet_init_init_community(check_addresses, check_threshold), + } => community_wallet_init_init_community(initial_authorities, check_threshold), + CommunityWalletInitProposeOffer { + new_signers, + num_signers, + } => community_wallet_init_propose_offer(new_signers, num_signers), DiemGovernanceAddApprovedScriptHashScript { proposal_id } => { diem_governance_add_approved_script_hash_script(proposal_id) } @@ -689,10 +701,12 @@ impl EntryFunctionCall { LibraCoinClaimMintCapability {} => libra_coin_claim_mint_capability(), LibraCoinDelegateMintCapability { to } => libra_coin_delegate_mint_capability(to), LibraCoinMintToImpl { dst_addr, amount } => libra_coin_mint_to_impl(dst_addr, amount), - MultiActionFinalizeAndCage { - initial_authorities, - num_signers, - } => multi_action_finalize_and_cage(initial_authorities, num_signers), + MultiActionClaimOffer { multisig_address } => { + multi_action_claim_offer(multisig_address) + } + MultiActionMigrationMigrateOffer { multisig_address } => { + multi_action_migration_migrate_offer(multisig_address) + } MultisigAccountAddOwner { new_owner } => multisig_account_add_owner(new_owner), MultisigAccountAddOwners { new_owners } => multisig_account_add_owners(new_owners), MultisigAccountApproveTransaction { @@ -788,7 +802,7 @@ impl EntryFunctionCall { ProofOfFeePofUpdateBid { bid, epoch_expiry } => { proof_of_fee_pof_update_bid(bid, epoch_expiry) } - SafeInitPaymentMultisig {} => safe_init_payment_multisig(), + SafeInitPaymentMultisig { authorities } => safe_init_payment_multisig(authorities), SlowWalletSmokeTestVmUnlock { user_addr, unlocked, @@ -1174,10 +1188,7 @@ pub fn community_wallet_init_change_signer_community_multisig( /// convenience function to check if the account can be caged /// after all the structs are in place -pub fn community_wallet_init_finalize_and_cage( - initial_authorities: Vec, - num_signers: u64, -) -> TransactionPayload { +pub fn community_wallet_init_finalize_and_cage(num_signers: u64) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( ModuleId::new( AccountAddress::new([ @@ -1188,15 +1199,12 @@ pub fn community_wallet_init_finalize_and_cage( ), ident_str!("finalize_and_cage").to_owned(), vec![], - vec![ - bcs::to_bytes(&initial_authorities).unwrap(), - bcs::to_bytes(&num_signers).unwrap(), - ], + vec![bcs::to_bytes(&num_signers).unwrap()], )) } pub fn community_wallet_init_init_community( - check_addresses: Vec, + initial_authorities: Vec, check_threshold: u64, ) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( @@ -1210,12 +1218,34 @@ pub fn community_wallet_init_init_community( ident_str!("init_community").to_owned(), vec![], vec![ - bcs::to_bytes(&check_addresses).unwrap(), + bcs::to_bytes(&initial_authorities).unwrap(), bcs::to_bytes(&check_threshold).unwrap(), ], )) } +/// Propose offer to the multisig, and check if the signers are not related family +pub fn community_wallet_init_propose_offer( + new_signers: Vec, + num_signers: u64, +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("community_wallet_init").to_owned(), + ), + ident_str!("propose_offer").to_owned(), + vec![], + vec![ + bcs::to_bytes(&new_signers).unwrap(), + bcs::to_bytes(&num_signers).unwrap(), + ], + )) +} + pub fn diem_governance_add_approved_script_hash_script(proposal_id: u64) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( ModuleId::new( @@ -1564,11 +1594,7 @@ pub fn libra_coin_mint_to_impl(dst_addr: AccountAddress, amount: u64) -> Transac )) } -/// finalize the account and put in a cage. Will abort if governance has not -pub fn multi_action_finalize_and_cage( - initial_authorities: Vec, - num_signers: u64, -) -> TransactionPayload { +pub fn multi_action_claim_offer(multisig_address: AccountAddress) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( ModuleId::new( AccountAddress::new([ @@ -1577,12 +1603,26 @@ pub fn multi_action_finalize_and_cage( ]), ident_str!("multi_action").to_owned(), ), - ident_str!("finalize_and_cage").to_owned(), + ident_str!("claim_offer").to_owned(), vec![], - vec![ - bcs::to_bytes(&initial_authorities).unwrap(), - bcs::to_bytes(&num_signers).unwrap(), - ], + vec![bcs::to_bytes(&multisig_address).unwrap()], + )) +} + +pub fn multi_action_migration_migrate_offer( + multisig_address: AccountAddress, +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("multi_action_migration").to_owned(), + ), + ident_str!("migrate_offer").to_owned(), + vec![], + vec![bcs::to_bytes(&multisig_address).unwrap()], )) } @@ -2087,10 +2127,10 @@ pub fn proof_of_fee_pof_update_bid(bid: u64, epoch_expiry: u64) -> TransactionPa )) } -/// This fucntion initiates governance for the multisig. It is called by the sponsor address, and is only callable once. +/// This function initiates governance for the multisig. It is called by the sponsor address, and is only callable once. /// init_gov fails gracefully if the governance is already initialized. /// init_type will throw errors if the type is already initialized. -pub fn safe_init_payment_multisig() -> TransactionPayload { +pub fn safe_init_payment_multisig(authorities: Vec) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( ModuleId::new( AccountAddress::new([ @@ -2101,7 +2141,7 @@ pub fn safe_init_payment_multisig() -> TransactionPayload { ), ident_str!("init_payment_multisig").to_owned(), vec![], - vec![], + vec![bcs::to_bytes(&authorities).unwrap()], )) } @@ -2321,7 +2361,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::AccountOfferRotationCapability { - rotation_capability_sig_bytes: bcs::from_bytes(script.args().first()?).ok()?, + rotation_capability_sig_bytes: bcs::from_bytes(script.args().get(0)?).ok()?, account_scheme: bcs::from_bytes(script.args().get(1)?).ok()?, account_public_key_bytes: bcs::from_bytes(script.args().get(2)?).ok()?, recipient_address: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2336,7 +2376,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::AccountOfferSignerCapability { - signer_capability_sig_bytes: bcs::from_bytes(script.args().first()?).ok()?, + signer_capability_sig_bytes: bcs::from_bytes(script.args().get(0)?).ok()?, account_scheme: bcs::from_bytes(script.args().get(1)?).ok()?, account_public_key_bytes: bcs::from_bytes(script.args().get(2)?).ok()?, recipient_address: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2371,7 +2411,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::AccountRevokeRotationCapability { - to_be_revoked_address: bcs::from_bytes(script.args().first()?).ok()?, + to_be_revoked_address: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2383,7 +2423,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::AccountRevokeSignerCapability { - to_be_revoked_address: bcs::from_bytes(script.args().first()?).ok()?, + to_be_revoked_address: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2395,7 +2435,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::AccountRotateAuthenticationKey { - from_scheme: bcs::from_bytes(script.args().first()?).ok()?, + from_scheme: bcs::from_bytes(script.args().get(0)?).ok()?, from_public_key_bytes: bcs::from_bytes(script.args().get(1)?).ok()?, to_scheme: bcs::from_bytes(script.args().get(2)?).ok()?, to_public_key_bytes: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2413,7 +2453,7 @@ mod decoder { if let TransactionPayload::EntryFunction(script) = payload { Some( EntryFunctionCall::AccountRotateAuthenticationKeyWithRotationCapability { - rotation_cap_offerer_address: bcs::from_bytes(script.args().first()?).ok()?, + rotation_cap_offerer_address: bcs::from_bytes(script.args().get(0)?).ok()?, new_scheme: bcs::from_bytes(script.args().get(1)?).ok()?, new_public_key_bytes: bcs::from_bytes(script.args().get(2)?).ok()?, cap_update_table: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2427,7 +2467,7 @@ mod decoder { pub fn burn_set_send_community(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::BurnSetSendCommunity { - community: bcs::from_bytes(script.args().first()?).ok()?, + community: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2437,7 +2477,7 @@ mod decoder { pub fn code_publish_package_txn(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::CodePublishPackageTxn { - metadata_serialized: bcs::from_bytes(script.args().first()?).ok()?, + metadata_serialized: bcs::from_bytes(script.args().get(0)?).ok()?, code: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2448,8 +2488,8 @@ mod decoder { pub fn coin_transfer(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::CoinTransfer { - coin_type: script.ty_args().first()?.clone(), - to: bcs::from_bytes(script.args().first()?).ok()?, + coin_type: script.ty_args().get(0)?.clone(), + to: bcs::from_bytes(script.args().get(0)?).ok()?, amount: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2463,7 +2503,7 @@ mod decoder { if let TransactionPayload::EntryFunction(script) = payload { Some( EntryFunctionCall::CommunityWalletInitChangeSignerCommunityMultisig { - multisig_address: bcs::from_bytes(script.args().first()?).ok()?, + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, new_signer: bcs::from_bytes(script.args().get(1)?).ok()?, is_add_operation: bcs::from_bytes(script.args().get(2)?).ok()?, n_of_m: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2480,8 +2520,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::CommunityWalletInitFinalizeAndCage { - initial_authorities: bcs::from_bytes(script.args().first()?).ok()?, - num_signers: bcs::from_bytes(script.args().get(1)?).ok()?, + num_signers: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2493,7 +2532,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::CommunityWalletInitInitCommunity { - check_addresses: bcs::from_bytes(script.args().first()?).ok()?, + initial_authorities: bcs::from_bytes(script.args().get(0)?).ok()?, check_threshold: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2501,13 +2540,26 @@ mod decoder { } } + pub fn community_wallet_init_propose_offer( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::CommunityWalletInitProposeOffer { + new_signers: bcs::from_bytes(script.args().get(0)?).ok()?, + num_signers: bcs::from_bytes(script.args().get(1)?).ok()?, + }) + } else { + None + } + } + pub fn diem_governance_add_approved_script_hash_script( payload: &TransactionPayload, ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some( EntryFunctionCall::DiemGovernanceAddApprovedScriptHashScript { - proposal_id: bcs::from_bytes(script.args().first()?).ok()?, + proposal_id: bcs::from_bytes(script.args().get(0)?).ok()?, }, ) } else { @@ -2520,7 +2572,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DiemGovernanceAssertCanResolve { - proposal_id: bcs::from_bytes(script.args().first()?).ok()?, + proposal_id: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2532,7 +2584,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DiemGovernanceCreateProposalV2 { - execution_hash: bcs::from_bytes(script.args().first()?).ok()?, + execution_hash: bcs::from_bytes(script.args().get(0)?).ok()?, metadata_location: bcs::from_bytes(script.args().get(1)?).ok()?, metadata_hash: bcs::from_bytes(script.args().get(2)?).ok()?, is_multi_step_proposal: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2547,7 +2599,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DiemGovernanceOlCreateProposalV2 { - execution_hash: bcs::from_bytes(script.args().first()?).ok()?, + execution_hash: bcs::from_bytes(script.args().get(0)?).ok()?, metadata_location: bcs::from_bytes(script.args().get(1)?).ok()?, metadata_hash: bcs::from_bytes(script.args().get(2)?).ok()?, is_multi_step_proposal: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2560,7 +2612,7 @@ mod decoder { pub fn diem_governance_ol_vote(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DiemGovernanceOlVote { - proposal_id: bcs::from_bytes(script.args().first()?).ok()?, + proposal_id: bcs::from_bytes(script.args().get(0)?).ok()?, should_pass: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2591,7 +2643,7 @@ mod decoder { pub fn diem_governance_vote(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DiemGovernanceVote { - proposal_id: bcs::from_bytes(script.args().first()?).ok()?, + proposal_id: bcs::from_bytes(script.args().get(0)?).ok()?, should_pass: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2604,7 +2656,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DonorVoiceTxsProposeLiquidateTx { - multisig_address: bcs::from_bytes(script.args().first()?).ok()?, + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2616,7 +2668,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DonorVoiceTxsProposePaymentTx { - multisig_address: bcs::from_bytes(script.args().first()?).ok()?, + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, payee: bcs::from_bytes(script.args().get(1)?).ok()?, value: bcs::from_bytes(script.args().get(2)?).ok()?, description: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2631,7 +2683,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DonorVoiceTxsProposeVetoTx { - multisig_address: bcs::from_bytes(script.args().first()?).ok()?, + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, id: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2644,7 +2696,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DonorVoiceTxsVoteLiquidationTx { - multisig_address: bcs::from_bytes(script.args().first()?).ok()?, + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2654,7 +2706,7 @@ mod decoder { pub fn donor_voice_txs_vote_veto_tx(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DonorVoiceTxsVoteVetoTx { - multisig_address: bcs::from_bytes(script.args().first()?).ok()?, + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, id: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2675,7 +2727,7 @@ mod decoder { pub fn jail_unjail_by_voucher(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::JailUnjailByVoucher { - addr: bcs::from_bytes(script.args().first()?).ok()?, + addr: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2697,7 +2749,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::LibraCoinDelegateMintCapability { - to: bcs::from_bytes(script.args().first()?).ok()?, + to: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2707,7 +2759,7 @@ mod decoder { pub fn libra_coin_mint_to_impl(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::LibraCoinMintToImpl { - dst_addr: bcs::from_bytes(script.args().first()?).ok()?, + dst_addr: bcs::from_bytes(script.args().get(0)?).ok()?, amount: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2715,13 +2767,22 @@ mod decoder { } } - pub fn multi_action_finalize_and_cage( + pub fn multi_action_claim_offer(payload: &TransactionPayload) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::MultiActionClaimOffer { + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, + }) + } else { + None + } + } + + pub fn multi_action_migration_migrate_offer( payload: &TransactionPayload, ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { - Some(EntryFunctionCall::MultiActionFinalizeAndCage { - initial_authorities: bcs::from_bytes(script.args().first()?).ok()?, - num_signers: bcs::from_bytes(script.args().get(1)?).ok()?, + Some(EntryFunctionCall::MultiActionMigrationMigrateOffer { + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2731,7 +2792,7 @@ mod decoder { pub fn multisig_account_add_owner(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountAddOwner { - new_owner: bcs::from_bytes(script.args().first()?).ok()?, + new_owner: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2741,7 +2802,7 @@ mod decoder { pub fn multisig_account_add_owners(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountAddOwners { - new_owners: bcs::from_bytes(script.args().first()?).ok()?, + new_owners: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2753,7 +2814,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountApproveTransaction { - multisig_account: bcs::from_bytes(script.args().first()?).ok()?, + multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?, sequence_number: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2764,7 +2825,7 @@ mod decoder { pub fn multisig_account_create(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountCreate { - num_signatures_required: bcs::from_bytes(script.args().first()?).ok()?, + num_signatures_required: bcs::from_bytes(script.args().get(0)?).ok()?, metadata_keys: bcs::from_bytes(script.args().get(1)?).ok()?, metadata_values: bcs::from_bytes(script.args().get(2)?).ok()?, }) @@ -2778,7 +2839,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountCreateTransaction { - multisig_account: bcs::from_bytes(script.args().first()?).ok()?, + multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?, payload: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2792,7 +2853,7 @@ mod decoder { if let TransactionPayload::EntryFunction(script) = payload { Some( EntryFunctionCall::MultisigAccountCreateTransactionWithHash { - multisig_account: bcs::from_bytes(script.args().first()?).ok()?, + multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?, payload_hash: bcs::from_bytes(script.args().get(1)?).ok()?, }, ) @@ -2807,7 +2868,7 @@ mod decoder { if let TransactionPayload::EntryFunction(script) = payload { Some( EntryFunctionCall::MultisigAccountCreateWithExistingAccount { - multisig_address: bcs::from_bytes(script.args().first()?).ok()?, + multisig_address: bcs::from_bytes(script.args().get(0)?).ok()?, owners: bcs::from_bytes(script.args().get(1)?).ok()?, num_signatures_required: bcs::from_bytes(script.args().get(2)?).ok()?, account_scheme: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2828,7 +2889,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountCreateWithOwners { - additional_owners: bcs::from_bytes(script.args().first()?).ok()?, + additional_owners: bcs::from_bytes(script.args().get(0)?).ok()?, num_signatures_required: bcs::from_bytes(script.args().get(1)?).ok()?, metadata_keys: bcs::from_bytes(script.args().get(2)?).ok()?, metadata_values: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2844,7 +2905,7 @@ mod decoder { if let TransactionPayload::EntryFunction(script) = payload { Some( EntryFunctionCall::MultisigAccountExecuteRejectedTransaction { - multisig_account: bcs::from_bytes(script.args().first()?).ok()?, + multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?, }, ) } else { @@ -2857,7 +2918,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountMigrateWithOwners { - additional_owners: bcs::from_bytes(script.args().first()?).ok()?, + additional_owners: bcs::from_bytes(script.args().get(0)?).ok()?, num_signatures_required: bcs::from_bytes(script.args().get(1)?).ok()?, metadata_keys: bcs::from_bytes(script.args().get(2)?).ok()?, metadata_values: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -2872,7 +2933,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountRejectTransaction { - multisig_account: bcs::from_bytes(script.args().first()?).ok()?, + multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?, sequence_number: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2885,7 +2946,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountRemoveOwner { - owner_to_remove: bcs::from_bytes(script.args().first()?).ok()?, + owner_to_remove: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2897,7 +2958,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountRemoveOwners { - owners_to_remove: bcs::from_bytes(script.args().first()?).ok()?, + owners_to_remove: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2909,7 +2970,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountUpdateMetadata { - keys: bcs::from_bytes(script.args().first()?).ok()?, + keys: bcs::from_bytes(script.args().get(0)?).ok()?, values: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2922,7 +2983,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountUpdateSignaturesRequired { - new_num_signatures_required: bcs::from_bytes(script.args().first()?).ok()?, + new_num_signatures_required: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2934,7 +2995,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::MultisigAccountVoteTransanction { - multisig_account: bcs::from_bytes(script.args().first()?).ok()?, + multisig_account: bcs::from_bytes(script.args().get(0)?).ok()?, sequence_number: bcs::from_bytes(script.args().get(1)?).ok()?, approved: bcs::from_bytes(script.args().get(2)?).ok()?, }) @@ -2946,7 +3007,7 @@ mod decoder { pub fn object_transfer_call(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::ObjectTransferCall { - object: bcs::from_bytes(script.args().first()?).ok()?, + object: bcs::from_bytes(script.args().get(0)?).ok()?, to: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -2957,7 +3018,7 @@ mod decoder { pub fn ol_account_create_account(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::OlAccountCreateAccount { - auth_key: bcs::from_bytes(script.args().first()?).ok()?, + auth_key: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2969,7 +3030,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::OlAccountSetAllowDirectCoinTransfers { - allow: bcs::from_bytes(script.args().first()?).ok()?, + allow: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -2979,7 +3040,7 @@ mod decoder { pub fn ol_account_transfer(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::OlAccountTransfer { - to: bcs::from_bytes(script.args().first()?).ok()?, + to: bcs::from_bytes(script.args().get(0)?).ok()?, amount: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -3006,7 +3067,7 @@ mod decoder { pub fn proof_of_fee_pof_update_bid(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::ProofOfFeePofUpdateBid { - bid: bcs::from_bytes(script.args().first()?).ok()?, + bid: bcs::from_bytes(script.args().get(0)?).ok()?, epoch_expiry: bcs::from_bytes(script.args().get(1)?).ok()?, }) } else { @@ -3015,8 +3076,10 @@ mod decoder { } pub fn safe_init_payment_multisig(payload: &TransactionPayload) -> Option { - if let TransactionPayload::EntryFunction(_script) = payload { - Some(EntryFunctionCall::SafeInitPaymentMultisig {}) + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::SafeInitPaymentMultisig { + authorities: bcs::from_bytes(script.args().get(0)?).ok()?, + }) } else { None } @@ -3027,7 +3090,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::SlowWalletSmokeTestVmUnlock { - user_addr: bcs::from_bytes(script.args().first()?).ok()?, + user_addr: bcs::from_bytes(script.args().get(0)?).ok()?, unlocked: bcs::from_bytes(script.args().get(1)?).ok()?, transferred: bcs::from_bytes(script.args().get(2)?).ok()?, }) @@ -3047,7 +3110,7 @@ mod decoder { pub fn stake_initialize_validator(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::StakeInitializeValidator { - consensus_pubkey: bcs::from_bytes(script.args().first()?).ok()?, + consensus_pubkey: bcs::from_bytes(script.args().get(0)?).ok()?, proof_of_possession: bcs::from_bytes(script.args().get(1)?).ok()?, network_addresses: bcs::from_bytes(script.args().get(2)?).ok()?, fullnode_addresses: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -3060,7 +3123,7 @@ mod decoder { pub fn stake_rotate_consensus_key(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::StakeRotateConsensusKey { - validator_address: bcs::from_bytes(script.args().first()?).ok()?, + validator_address: bcs::from_bytes(script.args().get(0)?).ok()?, new_consensus_pubkey: bcs::from_bytes(script.args().get(1)?).ok()?, proof_of_possession: bcs::from_bytes(script.args().get(2)?).ok()?, }) @@ -3074,7 +3137,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::StakeUpdateNetworkAndFullnodeAddresses { - validator_address: bcs::from_bytes(script.args().first()?).ok()?, + validator_address: bcs::from_bytes(script.args().get(0)?).ok()?, new_network_addresses: bcs::from_bytes(script.args().get(1)?).ok()?, new_fullnode_addresses: bcs::from_bytes(script.args().get(2)?).ok()?, }) @@ -3088,7 +3151,7 @@ mod decoder { ) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::ValidatorUniverseRegisterValidator { - consensus_pubkey: bcs::from_bytes(script.args().first()?).ok()?, + consensus_pubkey: bcs::from_bytes(script.args().get(0)?).ok()?, proof_of_possession: bcs::from_bytes(script.args().get(1)?).ok()?, network_addresses: bcs::from_bytes(script.args().get(2)?).ok()?, fullnode_addresses: bcs::from_bytes(script.args().get(3)?).ok()?, @@ -3101,7 +3164,7 @@ mod decoder { pub fn version_set_version(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::VersionSetVersion { - major: bcs::from_bytes(script.args().first()?).ok()?, + major: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -3111,7 +3174,7 @@ mod decoder { pub fn vouch_insist_vouch_for(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::VouchInsistVouchFor { - wanna_be_my_friend: bcs::from_bytes(script.args().first()?).ok()?, + wanna_be_my_friend: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -3121,7 +3184,7 @@ mod decoder { pub fn vouch_revoke(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::VouchRevoke { - its_not_me_its_you: bcs::from_bytes(script.args().first()?).ok()?, + its_not_me_its_you: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -3131,7 +3194,7 @@ mod decoder { pub fn vouch_vouch_for(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::VouchVouchFor { - wanna_be_my_friend: bcs::from_bytes(script.args().first()?).ok()?, + wanna_be_my_friend: bcs::from_bytes(script.args().get(0)?).ok()?, }) } else { None @@ -3207,6 +3270,10 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy acquires Offer { borrow_global(multisig_address).proposed } // Query claimed authorities for the given multisig address. + #[view] public fun get_offer_claimed(multisig_address: address): vector

acquires Offer { borrow_global(multisig_address).claimed } @@ -511,6 +513,7 @@ module ol_framework::multi_action { borrow_global(multisig_address).expiration_epoch } + #[view] public fun get_offer_proposed_n_of_m(multisig_address: address): Option acquires Offer { borrow_global(multisig_address).proposed_n_of_m } @@ -913,9 +916,9 @@ module ol_framework::multi_action { maybe_update_threshold_after_claim(multisig_address, n_of_m); } - // If authorities voted to change the number of signatures required along authorities addition, + // If authorities voted to change the number of signatures required along authorities addition, // new authorities must claim the offer before the number of signatures required is applied. - fun maybe_update_threshold_after_claim(multisig_address: address, n_of_m: &Option) acquires Offer { + fun maybe_update_threshold_after_claim(multisig_address: address, n_of_m: &Option) acquires Offer { if (option::is_some(n_of_m)) { let new_n_of_m = *option::borrow(n_of_m); let current_n_of_m = multisig_account::num_signatures_required(multisig_address); @@ -935,9 +938,9 @@ module ol_framework::multi_action { fun maybe_update_threshold(multisig_address: address, governance: &mut Governance, n_of_m_opt: &Option) acquires Offer { if (option::is_some(n_of_m_opt)) { - multisig_account::multi_auth_helper_update_signatures_required(&governance.guid_capability, + multisig_account::multi_auth_helper_update_signatures_required(&governance.guid_capability, *option::borrow(n_of_m_opt)); - + // clean the Offer n_of_m to avoid a future claim change the n_of_m let offer = borrow_global_mut(multisig_address); offer.proposed_n_of_m = option::none(); @@ -1054,4 +1057,4 @@ module ol_framework::multi_action { multisig_account::migrate_with_owners(sig, initial_authorities, num_signers, vector::empty(), vector::empty()); } -} \ No newline at end of file +} diff --git a/framework/move-stdlib/sources/error.move b/framework/move-stdlib/sources/error.move index 1facaf01d..0659e241f 100644 --- a/framework/move-stdlib/sources/error.move +++ b/framework/move-stdlib/sources/error.move @@ -9,7 +9,7 @@ /// /// ``` /// /// An invalid ASCII character was encountered when creating a string. -/// const EINVALID_CHARACTER: u64 = 0x010003; +/// const EINVALID_CHARACTER: u64 = 0x10003; /// ``` /// /// This code is both valid in the worlds with and without canonical errors. It can be used as a plain module local diff --git a/tools/txs/src/txs_cli_community.rs b/tools/txs/src/txs_cli_community.rs index 35faa70b1..161283975 100644 --- a/tools/txs/src/txs_cli_community.rs +++ b/tools/txs/src/txs_cli_community.rs @@ -13,14 +13,13 @@ pub enum CommunityTxs { /// Initialize a DonorVoice multi-sig by proposing an offer to initial authorities. /// NOTE: Then authorities need to claim the offer, and the donor needs to cage the account to become a multi-sig account. GovInit(InitTx), - /// Update proposed offer to initial authorities - /*GovOffer(OfferTx), + /*/// Update proposed offer to initial authorities + GovOffer(OfferTx),*/ /// Claim the proposed offer GovClaim(ClaimTx), /// Finalize and cage the multisig account after authorities claim the offer GovCage(CageTx), /// Propose a change to the authorities of the DonorVoice multi-sig - */ GovAdmin(AdminTx), /// Propose a multi-sig transaction Propose(ProposeTx), @@ -44,7 +43,7 @@ impl CommunityTxs { Err(e) => { println!("ERROR: could not propose offer, message: {}", e); } - }, + },*/ CommunityTxs::GovClaim(claim) => match claim.run(sender).await { Ok(_) => println!("SUCCESS: community wallet offer claimed"), Err(e) => { @@ -56,7 +55,7 @@ impl CommunityTxs { Err(e) => { println!("ERROR: could not finalize wallet, message: {}", e); } - },*/ + }, CommunityTxs::GovAdmin(admin) => match admin.run(sender).await { Ok(_) => println!("SUCCESS: community wallet admin added"), Err(e) => { @@ -98,15 +97,11 @@ pub struct InitTx { #[clap(short, long)] /// Num of signatures needed for the n-of-m pub num_signers: u64, - - #[clap(long)] - /// Finalize the configurations and rotate the auth key, not reversible! - pub finalize: bool, } impl InitTx { pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { - if self.finalize { + /*if self.finalize { // Warning message println!("\nWARNING: This operation will finalize the account associated with the governance-initialized wallet and make it inaccessible. This action is IRREVERSIBLE and can only be applied to a wallet where governance has been initialized.\n"); @@ -118,20 +113,52 @@ impl InitTx { // Execute the transaction sender.sign_submit_wait(payload).await?; println!("The account has been finalized and caged."); - } else { - let payload = libra_stdlib::community_wallet_init_init_community( - self.admins.clone(), - self.num_signers, - ); + } else {*/ + let payload = libra_stdlib::community_wallet_init_init_community( + self.admins.clone(), + self.num_signers, + ); - sender.sign_submit_wait(payload).await?; - println!("You have completed the first step in creating a community wallet, now you should check your work and finalize with --finalize"); - } + sender.sign_submit_wait(payload).await?; + println!("You have completed the first step in creating a community wallet, now you should check your work and finalize with --finalize"); + + Ok(()) + } +} + +#[derive(clap::Args)] +/// Claim the offer to become an authority in the multi-sig +pub struct ClaimTx { + #[clap(short, long)] + /// The Community Wallet to claim the offer + pub community_wallet: AccountAddress, +} + +impl ClaimTx { + pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { + let payload = libra_stdlib::multi_action_claim_offer(self.community_wallet); + sender.sign_submit_wait(payload).await?; + Ok(()) + } +} + +#[derive(clap::Args)] +/// Finalize and cage the community wallet to become multisig +pub struct CageTx { + #[clap(short, long)] + /// Num of signatures needed for the n-of-m + pub num_signers: u64, +} +impl CageTx { + pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { + let payload = libra_stdlib::community_wallet_init_finalize_and_cage(self.num_signers); + sender.sign_submit_wait(payload).await?; Ok(()) } } + #[derive(clap::Args)] pub struct AdminTx { #[clap(short, long)] @@ -429,4 +456,3 @@ impl VetoTx { Ok(()) } } - diff --git a/tools/txs/tests/cw_temp.rs b/tools/txs/tests/cw_temp.rs new file mode 100644 index 000000000..370b9c417 --- /dev/null +++ b/tools/txs/tests/cw_temp.rs @@ -0,0 +1,260 @@ +use diem_crypto::ValidCryptoMaterialStringExt; +use diem_types::account_address::AccountAddress; +use diem_temppath::TempPath; +use libra_query::query_view; +use libra_smoke_tests::{configure_validator, libra_smoke::LibraSmoke}; +use libra_txs::txs_cli::{TxsCli, TxsSub, TxsSub::Transfer}; +use libra_txs::txs_cli_community::{ + CommunityTxs, InitTx, ClaimTx, CageTx +}; +use libra_types::legacy_types::app_cfg::TxCost; + +// Create a V7 community wallet +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_community_wallet() -> Result<(), anyhow::Error> { + let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; + + // SETUP ADMIN SIGNERS + // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. + // 2. Transfer funds to the newly created signer accounts to enable their transactional capabilities. + + // SETUP COMMUNITY WALLET + // 3. Prepare a new admin account but do not immediately use it within the community wallet. + // 4. Create a community wallet offering the first three of the newly funded accounts as its admins. + // 5. Admins claim the offer. + // 6. Donor finalize and cage the community wallet to ensure its independence and security. + + // SETUP ADMIN SIGNERS // + // We set up 5 new accounts and also fund them from each of the 5 validators + + let (signers, signer_addresses) = s.create_accounts(5).await?; + + // Ensure there's a one-to-one correspondence between signers and private keys + if signer_addresses.len() != s.validator_private_keys.len() { + panic!("The number of signer addresses does not match the number of validator private keys."); + } + + for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { + let to_account = signer_address.clone(); + + // Transfer funds to ensure the account exists on-chain using the specific validator's private key + let cli_transfer = TxsCli { + subcommand: Some(Transfer { + to_account, + amount: 10.0, + }), + mnemonic: None, + test_private_key: Some(validator_private_key.clone()), + chain_id: None, + config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), + url: Some(s.api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + // Execute the transfer + cli_transfer.run() + .await + .expect(&format!("CLI could not transfer funds to account {}", signer_address)); + } + + // SETUP COMMUNITY WALLET // + + // Prepare new admin account + let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; + + let new_admin_address = AccountAddress::from_hex_literal(new_admin) + .expect("Failed to parse account address"); + + // Fund with the last signer to avoid ancestry issues + let private_key_of_fifth_signer = signers[4] + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); + + // Transfer funds to ensure the account exists on-chain + let cli_transfer = TxsCli { + subcommand: Some(Transfer { + to_account: new_admin_address, + amount: 1.0, + }), + mnemonic: None, + test_private_key: Some(private_key_of_fifth_signer), + chain_id: None, + config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), + url: Some(s.api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_transfer.run() + .await + .expect("CLI could not transfer funds to the new account"); + + // Get 3 signers to be admins + let first_three_signer_addresses: Vec = signer_addresses + .clone() + .into_iter() + .take(3) + .collect(); + + // Create new community wallet and offer it to the first three signers + let cli_set_community_wallet = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovInit(InitTx { + admins: first_three_signer_addresses.clone(), + num_signers: 3, + }))), + mnemonic: None, + test_private_key: Some(s.encoded_pri_key.clone()), + chain_id: None, + config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), + url: Some(s.api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_set_community_wallet.run() + .await + .expect("CLI could not create community wallet"); + + // Verify if the account is not a community wallet yet + let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet init check"); + + assert!(!is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should not be a community wallet yet"); + + // Check offer proposed + let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_proposed", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet proposed offer"); + + // Assert authorities are the three proposed + let authorities = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities.len(), 3, "There should be 3 authorities"); + for i in 0..3 { + assert_eq!(authorities[i].as_str().unwrap().trim_start_matches("0x"), first_three_signer_addresses[i].to_string(), "Authority should be the same"); + } + + // Admins claim the offer + for j in 0..3 { + let auth = &signers[j]; + // print private key + let cli_claim_offer = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovClaim(ClaimTx { + community_wallet: comm_wallet_addr.clone(), + }))), + mnemonic: None, + test_private_key: Some(auth.private_key().to_encoded_string().expect("cannot decode pri key")), + chain_id: None, + config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), + url: Some(s.api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_claim_offer.run() + .await + .expect("CLI could not claim offer"); + } + + // Check offer proposed + let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_claimed", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet offer claimed"); + + // Assert authorities are the three proposed + let authorities = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities.len(), 3, "There should be 3 authorities"); + for i in 0..3 { + assert_eq!(authorities[i].as_str().unwrap().trim_start_matches("0x"), first_three_signer_addresses[i].to_string(), "Authority should be the same"); + } + + // Donor finalize and cage the community wallet + let cli_finalize_cage = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovCage(CageTx { + num_signers: 3, + }))), + mnemonic: None, + test_private_key: Some(s.encoded_pri_key.clone()), + chain_id: None, + config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), + url: Some(s.api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_finalize_cage.run() + .await + .expect("CLI could not finalize and cage community wallet"); + + // Ensure the account is now a community wallet + let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet init check"); + + assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); + + Ok(()) +} + +// UTILITY // + +async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, AccountAddress) { + let dir = diem_temppath::TempPath::new(); + let mut s = LibraSmoke::new(Some(5), None) + .await + .expect("Could not start libra smoke"); + + configure_validator::init_val_config_files(&mut s.swarm, 0, dir.path().to_owned()) + .await + .expect("Could not initialize validator config"); + + let account_address = "0x029633a96b0c0e81cc26cf2baefdbd479dab7161fbd066ca3be850012342cdee"; + + let account_address_wrapped = + AccountAddress::from_hex_literal(account_address).expect("Failed to parse account address"); + + // Transfer funds to ensure the account exists on-chain + let cli_transfer = TxsCli { + subcommand: Some(Transfer { + to_account: account_address_wrapped, + amount: 100.0, + }), + mnemonic: None, + test_private_key: Some(s.encoded_pri_key.clone()), + chain_id: None, + config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), + url: Some(s.api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_transfer + .run() + .await + .expect("CLI could not transfer funds to the new account"); + + // get the address of the first node, the private key that was used to create the comm wallet + let first_node = s + .swarm + .validators() + .next() + .expect("no first validator") + .to_owned(); + let comm_wallet_addr = first_node.peer_id(); + + (s, dir, account_address_wrapped, comm_wallet_addr) +} From 61a7366fc7442c05e22fac9168e2d15a2c45c873 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:08:00 -0300 Subject: [PATCH 43/68] community wallet cli test refactoring --- tools/txs/tests/cw_temp.rs | 214 +++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 92 deletions(-) diff --git a/tools/txs/tests/cw_temp.rs b/tools/txs/tests/cw_temp.rs index 370b9c417..b170cd466 100644 --- a/tools/txs/tests/cw_temp.rs +++ b/tools/txs/tests/cw_temp.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use diem_crypto::ValidCryptoMaterialStringExt; use diem_types::account_address::AccountAddress; use diem_temppath::TempPath; @@ -8,11 +10,13 @@ use libra_txs::txs_cli_community::{ CommunityTxs, InitTx, ClaimTx, CageTx }; use libra_types::legacy_types::app_cfg::TxCost; +use url::Url; // Create a V7 community wallet #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn create_community_wallet() -> Result<(), anyhow::Error> { let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; + let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); // SETUP ADMIN SIGNERS // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. @@ -38,26 +42,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { let to_account = signer_address.clone(); // Transfer funds to ensure the account exists on-chain using the specific validator's private key - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account, - amount: 10.0, - }), - mnemonic: None, - test_private_key: Some(validator_private_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - // Execute the transfer - cli_transfer.run() - .await - .expect(&format!("CLI could not transfer funds to account {}", signer_address)); + run_cli_transfer(to_account, 10.0, validator_private_key.clone(), s.api_endpoint.clone(), config_path.clone()).await; } // SETUP COMMUNITY WALLET // @@ -75,25 +60,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { .expect("cannot decode pri key"); // Transfer funds to ensure the account exists on-chain - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account: new_admin_address, - amount: 1.0, - }), - mnemonic: None, - test_private_key: Some(private_key_of_fifth_signer), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - cli_transfer.run() - .await - .expect("CLI could not transfer funds to the new account"); + run_cli_transfer(new_admin_address, 1.0, private_key_of_fifth_signer.clone(), s.api_endpoint.clone(), config_path.clone()).await; // Get 3 signers to be admins let first_three_signer_addresses: Vec = signer_addresses @@ -103,25 +70,8 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { .collect(); // Create new community wallet and offer it to the first three signers - let cli_set_community_wallet = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovInit(InitTx { - admins: first_three_signer_addresses.clone(), - num_signers: 3, - }))), - mnemonic: None, - test_private_key: Some(s.encoded_pri_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - cli_set_community_wallet.run() - .await - .expect("CLI could not create community wallet"); + let donor_private_key = s.encoded_pri_key.clone(); + run_cli_community_init(donor_private_key.clone(), first_three_signer_addresses.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; // Verify if the account is not a community wallet yet let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) @@ -146,27 +96,11 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { for j in 0..3 { let auth = &signers[j]; // print private key - let cli_claim_offer = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovClaim(ClaimTx { - community_wallet: comm_wallet_addr.clone(), - }))), - mnemonic: None, - test_private_key: Some(auth.private_key().to_encoded_string().expect("cannot decode pri key")), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - cli_claim_offer.run() - .await - .expect("CLI could not claim offer"); + let authority_pk = auth.private_key().to_encoded_string().expect("cannot decode pri key"); + run_cli_claim_offer(authority_pk, comm_wallet_addr.clone(), s.api_endpoint.clone(), config_path.clone()).await; } - // Check offer proposed + // Check offer claimed let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_claimed", None, Some(comm_wallet_addr.clone().to_string())) .await .expect("Query failed: community wallet offer claimed"); @@ -179,36 +113,132 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { } // Donor finalize and cage the community wallet - let cli_finalize_cage = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovCage(CageTx { - num_signers: 3, - }))), + run_cli_community_cage(donor_private_key.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; + + // Ensure the account is now a community wallet + let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet init check"); + + assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); + + Ok(()) +} + +// UTILITY // + +async fn run_cli_transfer( + to_account: AccountAddress, + amount: f64, + private_key: String, + api_endpoint: Url, + config_path: PathBuf, +) { + // Build the CLI command + let cli_transfer = TxsCli { + subcommand: Some(Transfer { + to_account, + amount, + }), mnemonic: None, - test_private_key: Some(s.encoded_pri_key.clone()), + test_private_key: Some(private_key), chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), + config_path: Some(config_path), + url: Some(api_endpoint), tx_profile: None, tx_cost: Some(TxCost::default_baseline_cost()), estimate_only: false, legacy_address: false, }; - cli_finalize_cage.run() + // Execute the transfer + cli_transfer + .run() .await - .expect("CLI could not finalize and cage community wallet"); + .expect(&format!("CLI could not transfer funds to account {}", to_account.to_string())); +} - // Ensure the account is now a community wallet - let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) +async fn run_cli_community_init( + donor_private_key: String, + auhtorities: Vec, + num_signers: u64, + api_endpoint: Url, + config_path: PathBuf, +) { + // Build the CLI command + let cli_set_community_wallet = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovInit(InitTx { + admins: auhtorities, + num_signers: num_signers, + }))), + mnemonic: None, + test_private_key: Some(donor_private_key), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + // Execute the transaction + cli_set_community_wallet.run() .await - .expect("Query failed: community wallet init check"); + .expect("CLI could not create community wallet"); +} - assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); +async fn run_cli_claim_offer( + signer_pk: String, + community_address: AccountAddress, + api_endpoint: Url, + config_path: PathBuf +) { + let cli_claim_offer = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovClaim(ClaimTx { + community_wallet: community_address, + }))), + mnemonic: None, + test_private_key: Some(signer_pk), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; - Ok(()) + cli_claim_offer.run() + .await + .expect("CLI could not claim offer"); } -// UTILITY // +async fn run_cli_community_cage( + donor_private_key: String, + num_signers: u64, + api_endpoint: Url, + config_path: PathBuf +) { + let cli_finalize_cage = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovCage(CageTx { + num_signers: num_signers, + }))), + mnemonic: None, + test_private_key: Some(donor_private_key), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_finalize_cage.run() + .await + .expect("CLI could not finalize and cage community wallet"); +} async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, AccountAddress) { let dir = diem_temppath::TempPath::new(); From d4eadfdf5198a790d3d3a58775e0507155766e7b Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:25:11 -0300 Subject: [PATCH 44/68] prints a message after txs completed and update test comments --- tools/txs/src/txs_cli_community.rs | 17 +++-------------- tools/txs/tests/cw_temp.rs | 26 ++++++++++++++++++-------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/tools/txs/src/txs_cli_community.rs b/tools/txs/src/txs_cli_community.rs index 161283975..c9ff6268c 100644 --- a/tools/txs/src/txs_cli_community.rs +++ b/tools/txs/src/txs_cli_community.rs @@ -101,26 +101,13 @@ pub struct InitTx { impl InitTx { pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { - /*if self.finalize { - // Warning message - println!("\nWARNING: This operation will finalize the account associated with the governance-initialized wallet and make it inaccessible. This action is IRREVERSIBLE and can only be applied to a wallet where governance has been initialized.\n"); - - // Assuming the signer's account is already set in the `sender` object - // The payload for the finalize and cage operation - let payload = - libra_stdlib::community_wallet_init_finalize_and_cage(self.num_signers); // This function now does not require an account address - - // Execute the transaction - sender.sign_submit_wait(payload).await?; - println!("The account has been finalized and caged."); - } else {*/ let payload = libra_stdlib::community_wallet_init_init_community( self.admins.clone(), self.num_signers, ); sender.sign_submit_wait(payload).await?; - println!("You have completed the first step in creating a community wallet, now you should check your work and finalize with --finalize"); + println!("You have completed the first step in creating a community wallet, now the authorities you have proposed need to claim the offer."); Ok(()) } @@ -138,6 +125,7 @@ impl ClaimTx { pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { let payload = libra_stdlib::multi_action_claim_offer(self.community_wallet); sender.sign_submit_wait(payload).await?; + println!("You have claimed the community wallet offer."); Ok(()) } } @@ -154,6 +142,7 @@ impl CageTx { pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { let payload = libra_stdlib::community_wallet_init_finalize_and_cage(self.num_signers); sender.sign_submit_wait(payload).await?; + println!("The community wallet is finalized and caged. It is now a multi-sig account."); Ok(()) } } diff --git a/tools/txs/tests/cw_temp.rs b/tools/txs/tests/cw_temp.rs index b170cd466..edaa8ad07 100644 --- a/tools/txs/tests/cw_temp.rs +++ b/tools/txs/tests/cw_temp.rs @@ -24,13 +24,12 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { // SETUP COMMUNITY WALLET // 3. Prepare a new admin account but do not immediately use it within the community wallet. - // 4. Create a community wallet offering the first three of the newly funded accounts as its admins. + // 4. Initialize a community wallet offering the first three of the newly funded accounts as its admins. // 5. Admins claim the offer. // 6. Donor finalize and cage the community wallet to ensure its independence and security. // SETUP ADMIN SIGNERS // - // We set up 5 new accounts and also fund them from each of the 5 validators - + // 1. Generate and fund 5 new accounts let (signers, signer_addresses) = s.create_accounts(5).await?; // Ensure there's a one-to-one correspondence between signers and private keys @@ -38,6 +37,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { panic!("The number of signer addresses does not match the number of validator private keys."); } + // 2. Transfer funds to the newly created signer accounts for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { let to_account = signer_address.clone(); @@ -47,9 +47,8 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { // SETUP COMMUNITY WALLET // - // Prepare new admin account + // 3. Prepare a new admin account let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; - let new_admin_address = AccountAddress::from_hex_literal(new_admin) .expect("Failed to parse account address"); @@ -69,7 +68,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { .take(3) .collect(); - // Create new community wallet and offer it to the first three signers + // 4. Initialize the community wallet let donor_private_key = s.encoded_pri_key.clone(); run_cli_community_init(donor_private_key.clone(), first_three_signer_addresses.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; @@ -92,7 +91,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { assert_eq!(authorities[i].as_str().unwrap().trim_start_matches("0x"), first_three_signer_addresses[i].to_string(), "Authority should be the same"); } - // Admins claim the offer + // 5. Admins claim the offer. for j in 0..3 { let auth = &signers[j]; // print private key @@ -112,7 +111,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { assert_eq!(authorities[i].as_str().unwrap().trim_start_matches("0x"), first_three_signer_addresses[i].to_string(), "Authority should be the same"); } - // Donor finalize and cage the community wallet + // 6. Donor finalize and cage the community wallet run_cli_community_cage(donor_private_key.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; // Ensure the account is now a community wallet @@ -122,6 +121,17 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); + // Ensure authorities are the three proposed + let authrotities_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet authorities check"); + + let authorities = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities.len(), 3, "There should be 3 authorities"); + for i in 0..3 { + assert_eq!(authorities[i].as_str().unwrap().trim_start_matches("0x"), first_three_signer_addresses[i].to_string(), "Authority should be the same"); + } + Ok(()) } From 681b9f0d73e689dbdb4ab7e715fedc5f9fb823e1 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:57:40 -0300 Subject: [PATCH 45/68] adds propose_offer cli and smoke test update_community_wallet_offer --- framework/releases/head.mrb | Bin 863126 -> 863026 bytes tools/txs/src/txs_cli_community.rs | 35 +++++++-- tools/txs/tests/cw_temp.rs | 120 ++++++++++++++++++++++++++++- 3 files changed, 144 insertions(+), 11 deletions(-) diff --git a/framework/releases/head.mrb b/framework/releases/head.mrb index 670c4b5f21bb64314ebfe0d002f114e640657784..e8434d01659595ad3cfcf0408dfb7bdb71d7ceec 100644 GIT binary patch delta 27088 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 NZKCRfHaX-p{tr^lO`re( delta 27216 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

!O&|-HN-=VfVJJj7Il;xoaVyg|PiR=;*oi z5%0j}1;ZAkZ#HkS$t$^-l^y8hhrGVBcrH}Q&P3cb;nQ1^`E~NQIGB}F$E*1j z_?H+h_R3hgje0gw2MszZp%g?#wOam4=B}&jvYs7Zw6Uev3xet-$@0&Q8gOY=st&~{ ziJBZn8!#_G+LKMsjFgwFkn+R_Of|WfB&?(O*F9m;|J?07g%8!D1BT^bPqeR^qV$@K z)r*>RCCd)BP1mkgf9y;b5VyDM(U0^H4cK6-j*l)s0dyQY1q%N3_P2yOZ5?2 zY*%8XG{+dZTs}*A3c}02+=Kieydv3qTmAlCh7g8B6>{u8lbC9Cv5Ir~@4fMW#^QJa zTi<5iy`T&2Up5O_S^w%j>EE-|-xaR1s4^LRpSzqVykggXG@0N!t$cObO$BpFOEwOFX!|uEPzN5-N*0Tq*_M!5PM{jYtht=e#-x`RsmU;Kr za!tbF7Tv46+?0$icE*Id65>N0nRhY-cq`kuyriakn4ev!^_lhM#%tlPQ7#fD>EiV@bqm7bcZ7`$SV{Wk%7)ADR{B%uR%J@=(-CGbI4D0yOBWPn#QM)$?+B68t-S_xk zBc6`X%X&P;b76&01vihB$ZcE-qx;g>*VP3#TTYR~ri^N1Iq`GZL;A~v?i<8XynATh zXrwB6iBn-TQrW>hNQvFCF_E`$NS_#^4f8}ZT7!8(%n#tX`9qyg%1PGV=-``doo~hPRs}nrb5vue=KX2Sj31F z`Q~J(st631RJM5PD(Rh%-FYw{f;lw{bWvpCd^zc&X5!?Auwp3`C-6Y2Dt>v%quZ?55{{JCIwp*M@8ccw~TRT)CPedA$sPlfKdVHS1jH1(WwVd;N~)6fKa3Bc zEwF#9PHpJQim^;$rbOZ%G0fij-qahqd;U56TU7xr01WLYe z9fvxUbUkxbN|-JLffGy`a{DazCN5Gs_1ecG7|o0`J86%e;%#fhTgFDYazqkbbJ->o zoc7#UXG~{62EJ6wPfRH7fF30d&UZAD(|7CtkzKU3HdSf@y$^lIv@iudNq0=C6>5dX z?%f5(t=Uf#F2)5WYTYWzLk)^Yx zzWbYQH!PVxxuBbSXCscf{RV|T_Pu;)Wpno=^&g8|WtqtFzUdypj+)+%{+vFO%ONo|zxHEva;%bfmo5*NvpXQ^?Zo<@Yvt<;No@$1Kv8BV{vND5l0J zA>%I*8K^Cu*=sa@K5mcq$7Rw7EU8GA^bPsF!l(+w(CsL;V?~DMsqmB^*^w2=$0HX` znwDy@CoF=!n>Jwr$!#eJ@L&KBM({w^x22%$t^mzM-Vqd^%oZGxbm~4kL*}`2@TzmI z(9?f~=U9+~3Pz}WG@R)%x|ILa2U9`jZ>CHv%ccy((F7*$`lu0VxwH&uwvYuSqZbDh0`sP) zhahf3;SiV)SYp7*!vP3`ue<^Pq4xvmAt?V#iU0%^po9cvAW|8C5Ez00RRDs(ssRvK zFz5rlrLFF0J*e|r;zd>lxYt!pO^`J!Y&nON> z6KkxspbeA>X)CB@VsGML;$-3sb)y%Tlav!dDj+=sPa(Yoz3Eluka9kPI6*YhSJ2Ib zc# zAgov>00m_Rbf8ck@ccur)<679IzmMT0>m~EQz~oC{w_ic=aDj8!#cJ z{6k#@r}g+eluhvmFH0S}ShXB~cWmZo{A$kOi2?YTV2v+rr zNbni`e~ match offer.run(sender).await { + CommunityTxs::GovOffer(offer) => match offer.run(sender).await { Ok(_) => println!("SUCCESS: community wallet offer proposed"), Err(e) => { println!("ERROR: could not propose offer, message: {}", e); } - },*/ + }, CommunityTxs::GovClaim(claim) => match claim.run(sender).await { Ok(_) => println!("SUCCESS: community wallet offer claimed"), Err(e) => { @@ -87,8 +87,7 @@ impl CommunityTxs { } #[derive(clap::Args)] -/// Initialize a community wallet in two steps 1) make it a donor voice account, -/// and check proposed authorities 2) finalize and set the authorities +/// Initialize a community wallet offering the initial authorities pub struct InitTx { #[clap(short, long)] /// The initial admins of the multi-sig (cannot add self) @@ -113,6 +112,28 @@ impl InitTx { } } +#[derive(clap::Args)] +/// Propose offer to authorities to become an authority in the community wallet +pub struct OfferTx { + #[clap(short, long)] + /// The Community Wallet to propose the offer + pub admins: Vec, + /// Num of signatures needed for the n-of-m + pub num_signers: u64, +} + +impl OfferTx { + pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { + let payload = libra_stdlib::community_wallet_init_propose_offer( + self.admins.clone(), + self.num_signers, + ); + sender.sign_submit_wait(payload).await?; + println!("You have proposed the community wallet offer to the authorities."); + Ok(()) + } +} + #[derive(clap::Args)] /// Claim the offer to become an authority in the multi-sig pub struct ClaimTx { diff --git a/tools/txs/tests/cw_temp.rs b/tools/txs/tests/cw_temp.rs index edaa8ad07..aba2ddd8f 100644 --- a/tools/txs/tests/cw_temp.rs +++ b/tools/txs/tests/cw_temp.rs @@ -7,12 +7,12 @@ use libra_query::query_view; use libra_smoke_tests::{configure_validator, libra_smoke::LibraSmoke}; use libra_txs::txs_cli::{TxsCli, TxsSub, TxsSub::Transfer}; use libra_txs::txs_cli_community::{ - CommunityTxs, InitTx, ClaimTx, CageTx + CommunityTxs, InitTx, ClaimTx, CageTx, OfferTx }; use libra_types::legacy_types::app_cfg::TxCost; use url::Url; -// Create a V7 community wallet +// Happy day: create a v7 community wallet #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn create_community_wallet() -> Result<(), anyhow::Error> { let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; @@ -25,8 +25,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { // SETUP COMMUNITY WALLET // 3. Prepare a new admin account but do not immediately use it within the community wallet. // 4. Initialize a community wallet offering the first three of the newly funded accounts as its admins. - // 5. Admins claim the offer. - // 6. Donor finalize and cage the community wallet to ensure its independence and security. + // 5. Update the community wallet with a new admin account. // SETUP ADMIN SIGNERS // // 1. Generate and fund 5 new accounts @@ -135,6 +134,91 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { Ok(()) } +// Happy day: update community wallet offer before cage +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn update_community_wallet_offer() -> Result<(), anyhow::Error> { + let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; + let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); + + // SETUP ADMIN SIGNERS + // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. + // 2. Transfer funds to the newly created signer accounts to enable their transactional capabilities. + + // SETUP COMMUNITY WALLET + // 3. Prepare a new admin account but do not immediately use it within the community wallet. + // 4. Initialize a community wallet offering the first three of the newly funded accounts as its admins. + // 5. Admins claim the offer. + // 6. Donor finalize and cage the community wallet to ensure its independence and security. + + // SETUP ADMIN SIGNERS // + // 1. Generate and fund 5 new accounts + let (signers, signer_addresses) = s.create_accounts(5).await?; + + // Ensure there's a one-to-one correspondence between signers and private keys + if signer_addresses.len() != s.validator_private_keys.len() { + panic!("The number of signer addresses does not match the number of validator private keys."); + } + + // 2. Transfer funds to the newly created signer accounts + for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { + let to_account = signer_address.clone(); + + // Transfer funds to ensure the account exists on-chain using the specific validator's private key + run_cli_transfer(to_account, 10.0, validator_private_key.clone(), s.api_endpoint.clone(), config_path.clone()).await; + } + + // SETUP COMMUNITY WALLET // + + // 3. Prepare a new admin account + let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; + let new_admin_address = AccountAddress::from_hex_literal(new_admin) + .expect("Failed to parse account address"); + + // Fund with the last signer to avoid ancestry issues + let private_key_of_fifth_signer = signers[4] + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); + + // Transfer funds to ensure the account exists on-chain + run_cli_transfer(new_admin_address, 1.0, private_key_of_fifth_signer.clone(), s.api_endpoint.clone(), config_path.clone()).await; + + // Get 3 signers to be admins + let mut authorities: Vec = signer_addresses + .clone() + .into_iter() + .take(3) + .collect(); + + // 4. Initialize the community wallet + let donor_private_key = s.encoded_pri_key.clone(); + run_cli_community_init(donor_private_key.clone(), authorities.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; + + // 5. Update the community wallet with a new admin account. + + // Add forth signer as admin + let forth_signer_address = signer_addresses[3].clone(); + authorities.push(forth_signer_address); + run_cli_community_propose_offer(donor_private_key.clone(), authorities.clone(), 4, s.api_endpoint.clone(), config_path.clone()).await; + + // Check offer proposed + let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_proposed", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet proposed offer"); + + // Assert authorities are the three proposed + let proposed = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + println!("{:?}", authorities); + println!("{:?}", proposed); + assert_eq!(proposed.len(), 4, "There should be 4 authorities"); + for i in 0..4 { + assert_eq!(proposed[i].as_str().unwrap().trim_start_matches("0x"), authorities[i].to_string(), "Authority should be the same"); + } + + Ok(()) +} + + // UTILITY // async fn run_cli_transfer( @@ -250,6 +334,34 @@ async fn run_cli_community_cage( .expect("CLI could not finalize and cage community wallet"); } +async fn run_cli_community_propose_offer( + donor_private_key: String, + authorities: Vec, + num_signers: u64, + api_endpoint: Url, + config_path: PathBuf +) { + let cli_propose_offer = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovOffer(OfferTx { + admins: authorities, + num_signers: num_signers, + }))), + mnemonic: None, + test_private_key: Some(donor_private_key), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_propose_offer.run() + .await + .expect("CLI could not propose offer"); +} + async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, AccountAddress) { let dir = diem_temppath::TempPath::new(); let mut s = LibraSmoke::new(Some(5), None) From efc904c7e3c08ae46d1606f9488bef9452b91d70 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:35:40 -0300 Subject: [PATCH 46/68] create test vote new signer to cw + migrate cw tests to community_wallet.rs --- tools/txs/src/txs_cli_community.rs | 4 +- ...munity_wallet.todo => community_wallet.rs} | 720 +++++++++++------- tools/txs/tests/cw_temp.rs | 412 ---------- 3 files changed, 447 insertions(+), 689 deletions(-) rename tools/txs/tests/{community_wallet.todo => community_wallet.rs} (76%) delete mode 100644 tools/txs/tests/cw_temp.rs diff --git a/tools/txs/src/txs_cli_community.rs b/tools/txs/src/txs_cli_community.rs index 91cbda6f5..ce2d1b8a7 100644 --- a/tools/txs/src/txs_cli_community.rs +++ b/tools/txs/src/txs_cli_community.rs @@ -57,9 +57,9 @@ impl CommunityTxs { } }, CommunityTxs::GovAdmin(admin) => match admin.run(sender).await { - Ok(_) => println!("SUCCESS: community wallet admin added"), + Ok(_) => println!("SUCCESS: community wallet admin proposed"), Err(e) => { - println!("ERROR: could not add admin, message: {}", e); + println!("ERROR: could not propose new admin, message: {}", e); } }, CommunityTxs::Propose(propose) => match propose.run(sender).await { diff --git a/tools/txs/tests/community_wallet.todo b/tools/txs/tests/community_wallet.rs similarity index 76% rename from tools/txs/tests/community_wallet.todo rename to tools/txs/tests/community_wallet.rs index e63ca46ea..588883f16 100644 --- a/tools/txs/tests/community_wallet.todo +++ b/tools/txs/tests/community_wallet.rs @@ -1,14 +1,18 @@ +use std::path::PathBuf; +use diem_sdk::types::LocalAccount; use diem_crypto::ValidCryptoMaterialStringExt; -use diem_temppath::TempPath; use diem_types::account_address::AccountAddress; +use diem_temppath::TempPath; use libra_query::query_view; -use libra_smoke_tests::{configure_validator, helpers::get_libra_balance, libra_smoke::LibraSmoke}; +use libra_smoke_tests::{configure_validator, libra_smoke::LibraSmoke}; use libra_txs::txs_cli::{TxsCli, TxsSub, TxsSub::Transfer}; use libra_txs::txs_cli_community::{ - AdminTx, CommunityTxs, FinalizeCageTx, InitTx, ProposeTx, VetoTx, + CommunityTxs, InitTx, ClaimTx, CageTx, OfferTx, AdminTx }; use libra_types::legacy_types::app_cfg::TxCost; +use url::Url; +/* /// TODO: Test the migration of an existing community wallet with n flag #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn migrate_community_wallet_with_flag() { @@ -282,13 +286,13 @@ async fn new_community_wallet_cant_transfer() -> Result<(), anyhow::Error> { } Ok(()) -} +}*/ -// PASSING -//Create a V7 community wallet +// Create a v7 community wallet #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn create_community_wallet() -> Result<(), anyhow::Error> { - let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; + let (mut s, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = setup_environment().await; + let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); // SETUP ADMIN SIGNERS // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. @@ -296,13 +300,11 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { // SETUP COMMUNITY WALLET // 3. Prepare a new admin account but do not immediately use it within the community wallet. - // 4. Create a community wallet specifying the first three of the newly funded accounts as its admins. - // 5. Confirm the successful creation of the community wallet and its recognition by the system. - // 6. Revoke the original creator account's access to ensure security and independence of the community wallet. + // 4. Initialize a community wallet offering the first three of the newly funded accounts as its admins. + // 5. Update the community wallet with a new admin account. // SETUP ADMIN SIGNERS // - // We set up 5 new accounts and also fund them from each of the 5 validators - + // 1. Generate and fund 5 new accounts let (signers, signer_addresses) = s.create_accounts(5).await?; // Ensure there's a one-to-one correspondence between signers and private keys @@ -310,102 +312,190 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { panic!("The number of signer addresses does not match the number of validator private keys."); } + // 2. Transfer funds to the newly created signer accounts for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { let to_account = signer_address.clone(); // Transfer funds to ensure the account exists on-chain using the specific validator's private key - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account, - amount: 10.0, - }), - mnemonic: None, - test_private_key: Some(validator_private_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - // Execute the transfer - cli_transfer.run() - .await - .expect(&format!("CLI could not transfer funds to account {}", signer_address)); + run_cli_transfer(to_account, 10.0, validator_private_key.clone(), s.api_endpoint.clone(), config_path.clone()).await; } // SETUP COMMUNITY WALLET // - // Prepare new admin account + // 3. Prepare a new admin account let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; - let new_admin_address = AccountAddress::from_hex_literal(new_admin) - .expect("Failed to parse account address"); + .expect("Failed to parse account address"); // Fund with the last signer to avoid ancestry issues let private_key_of_fifth_signer = signers[4] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); // Transfer funds to ensure the account exists on-chain - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account: new_admin_address, - amount: 1.0, - }), - mnemonic: None, - test_private_key: Some(private_key_of_fifth_signer), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - cli_transfer.run() - .await - .expect("CLI could not transfer funds to the new account"); + run_cli_transfer(new_admin_address, 1.0, private_key_of_fifth_signer.clone(), s.api_endpoint.clone(), config_path.clone()).await; // Get 3 signers to be admins let first_three_signer_addresses: Vec = signer_addresses - .clone() - .into_iter() - .take(3) - .collect(); + .clone() + .into_iter() + .take(3) + .collect(); - //create new community wallet - let cli_set_community_wallet = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovInit(InitTx { - admins:first_three_signer_addresses, - migrate_n: None - }))), - mnemonic: None, - test_private_key: Some(s.encoded_pri_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; + // 4. Initialize the community wallet + run_cli_community_init(comm_wallet_pk.clone(), first_three_signer_addresses.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; - cli_set_community_wallet.run() + // Verify if the account is not a community wallet yet + let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) .await - .expect("CLI could not create community wallet"); + .expect("Query failed: community wallet init check"); - // Verify if the account is a community wallet + assert!(!is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should not be a community wallet yet"); + + // Check offer proposed + let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_proposed", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet proposed offer"); + + // Assert authorities are the three proposed + let authorities = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities.len(), 3, "There should be 3 authorities"); + for i in 0..3 { + let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(authority_str, first_three_signer_addresses[i].to_string(), "Authority should be the same"); + } + + // 5. Admins claim the offer. + for j in 0..3 { + let auth = &signers[j]; + // print private key + let authority_pk = auth.private_key().to_encoded_string().expect("cannot decode pri key"); + run_cli_claim_offer(authority_pk, comm_wallet_addr.clone(), s.api_endpoint.clone(), config_path.clone()).await; + } + + // Check offer claimed + let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_claimed", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet offer claimed"); + + // Assert authorities are the three proposed + let authorities = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities.len(), 3, "There should be 3 authorities"); + for i in 0..3 { + let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(authority_str, first_three_signer_addresses[i].to_string(), "Authority should be the same"); + } + + // 6. Donor finalize and cage the community wallet + run_cli_community_cage(comm_wallet_pk.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; + + // Ensure the account is now a community wallet let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) .await .expect("Query failed: community wallet init check"); assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); + // Ensure authorities are the three proposed + let authrotities_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet authorities check"); + + let authorities = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities.len(), 3, "There should be 3 authorities"); + for i in 0..3 { + let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(authority_str, first_three_signer_addresses[i].to_string(), "Authority should be the same"); + } + + Ok(()) +} + +// Happy day: update community wallet offer before cage +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn update_community_wallet_offer() -> Result<(), anyhow::Error> { + let (mut s, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = setup_environment().await; + let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); + + // SETUP ADMIN SIGNERS + // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. + // 2. Transfer funds to the newly created signer accounts to enable their transactional capabilities. + + // SETUP COMMUNITY WALLET + // 3. Prepare a new admin account but do not immediately use it within the community wallet. + // 4. Initialize a community wallet offering the first three of the newly funded accounts as its admins. + // 5. Admins claim the offer. + // 6. Donor finalize and cage the community wallet to ensure its independence and security. + + // SETUP ADMIN SIGNERS // + // 1. Generate and fund 5 new accounts + let (signers, signer_addresses) = s.create_accounts(5).await?; + + // Ensure there's a one-to-one correspondence between signers and private keys + if signer_addresses.len() != s.validator_private_keys.len() { + panic!("The number of signer addresses does not match the number of validator private keys."); + } + + // 2. Transfer funds to the newly created signer accounts + for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { + let to_account = signer_address.clone(); + + // Transfer funds to ensure the account exists on-chain using the specific validator's private key + run_cli_transfer(to_account, 10.0, validator_private_key.clone(), s.api_endpoint.clone(), config_path.clone()).await; + } + + // SETUP COMMUNITY WALLET // + + // 3. Prepare a new admin account + let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; + let new_admin_address = AccountAddress::from_hex_literal(new_admin) + .expect("Failed to parse account address"); + + // Fund with the last signer to avoid ancestry issues + let private_key_of_fifth_signer = signers[4] + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); + + // Transfer funds to ensure the account exists on-chain + run_cli_transfer(new_admin_address, 1.0, private_key_of_fifth_signer.clone(), s.api_endpoint.clone(), config_path.clone()).await; + + // Get 3 signers to be admins + let mut authorities: Vec = signer_addresses + .clone() + .into_iter() + .take(3) + .collect(); + + // 4. Initialize the community wallet + run_cli_community_init(comm_wallet_pk.clone(), authorities.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; + + // 5. Update the community wallet with a new admin account. + + // Add forth signer as admin + let forth_signer_address = signer_addresses[3].clone(); + authorities.push(forth_signer_address); + run_cli_community_propose_offer(comm_wallet_pk.clone(), authorities.clone(), 4, s.api_endpoint.clone(), config_path.clone()).await; + + // Check offer proposed + let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_proposed", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet proposed offer"); + + // Assert authorities are the three proposed + let proposed = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(proposed.len(), 4, "There should be 4 authorities"); + for i in 0..4 { + let proposed_str = &proposed[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(proposed_str, authorities[i].to_string(), "Authority should be the same"); + } + Ok(()) } +/* + // TODO: apply once we have a method to progress epochs // Propose and sign payment #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -579,246 +669,182 @@ async fn community_wallet_payment() -> Result<(), anyhow::Error> { Ok(()) } -// TODO: apply once we have a method to progress epochs -// Add an admin -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { - let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; - let client = s.client(); +*/ - // SETUP ADMIN SIGNERS - // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. - // 2. Transfer funds to the newly created signer accounts to enable their transactional capabilities. +async fn setup_community_wallet_caged(donor_pk: String, donor_address: AccountAddress, authorities: &Vec<&LocalAccount>, num_signitures: u64, config_path: PathBuf, api_endpoint: Url) { - // SETUP COMMUNITY WALLET - // 3. Prepare a new admin account but do not immediately use it within the community wallet. - // 4. Create a community wallet specifying the first three of the newly funded accounts as its admins. - // 5. Confirm the successful creation of the community wallet and its recognition by the system. - // 6. Revoke the original creator account's access to ensure security and independence of the community wallet. - - // ADD NEW ADMIN - // 7. Initiate the process to add a new admin to the community wallet by proposing through an existing admin. - // 8. Validate the addition of the new admin by checking the updated count of admins/signers in the wallet. + // 1. Initialize the community wallet + let authorities_addresses = authorities.iter().map(|a| a.address()).collect(); + run_cli_community_init(donor_pk.clone(), authorities_addresses, num_signitures, api_endpoint.clone(), config_path.clone()).await; - // SETUP ADMIN SIGNERS // - // We set up 5 new accounts and also fund them from each of the 5 validators + // 2. Admins claim the offer. + for j in 0..3 { + let auth = &authorities[j]; + // print private key + let authority_pk = auth.private_key().to_encoded_string().expect("cannot decode pri key"); + run_cli_claim_offer(authority_pk, donor_address.clone(), api_endpoint.clone(), config_path.clone()).await; + } - let (signers, signer_addresses) = s.create_accounts(5).await?; + // 3. Donor finalize and cage the community wallet + run_cli_community_cage(donor_pk, 3, api_endpoint, config_path).await; +} - // Ensure there's a one-to-one correspondence between signers and private keys - if signer_addresses.len() != s.validator_private_keys.len() { - panic!("The number of signer addresses does not match the number of validator private keys."); - } +// Add an admin +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { - for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { - let to_account = signer_address.clone(); // Adjust this line if necessary + // 1. Setup environment + let (mut smoke, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = setup_environment().await; + let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); + let api_endpoint = smoke.api_endpoint.clone(); + let client = smoke.client(); + // 2. Setup 4 funded accounts + let (signers, addresses) = smoke.create_accounts(5).await?; + for (signer_address, validator_private_key) in addresses.iter().zip(smoke.validator_private_keys.iter()) { + let to_account = signer_address.clone(); // Transfer funds to ensure the account exists on-chain using the specific validator's private key - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account, // Use the current signer address for this iteration - amount: 10.0, // Adjust the amount as needed - }), - mnemonic: None, - test_private_key: Some(validator_private_key.clone()), // Use the corresponding validator's private key - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - // Execute the transfer - cli_transfer.run() - .await - .expect(&format!("CLI could not transfer funds to account {}", signer_address)); + run_cli_transfer(to_account, 10.0, validator_private_key.clone(), smoke.api_endpoint.clone(), config_path.clone()).await; } - // SETUP COMMUNITY WALLET // - - // Prepare new admin account - let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; - - let new_admin_address = AccountAddress::from_hex_literal(new_admin) - .expect("Failed to parse account address"); - - // Fund with the last signer to avoid ancestry issues - let private_key_of_fifth_signer = signers[4] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); - - // Transfer funds to ensure the account exists on-chain - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account: new_admin_address, - amount: 1.0, - }), - mnemonic: None, - test_private_key: Some(private_key_of_fifth_signer), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - cli_transfer.run() - .await - .expect("CLI could not transfer funds to the new account"); - - // Get 3 signers to be admins - let first_three_signer_addresses: Vec = signer_addresses - .clone() - .into_iter() - .take(3) - .collect(); - - //create new community wallet - let cli_set_community_wallet = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovInit(InitTx { - admins:first_three_signer_addresses, - migrate_n: None - }))), - mnemonic: None, - test_private_key: Some(s.encoded_pri_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - cli_set_community_wallet.run() - .await - .expect("CLI could not create community wallet"); - - // Verify if the account is a community wallet - let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); - - assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); - - // Remove the ability for the original account to access - let cli_finalize_cage = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::FinalizeAndCage(FinalizeCageTx {}))), - mnemonic: None, - test_private_key: Some(s.encoded_pri_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - cli_finalize_cage.run() - .await - .expect("CLI could not finalize and cage the community wallet"); - - // ADD NEW ADMIN // + // 3. Setup community wallet caged with 3 authorities and 2 signitures + let initial_authorities: Vec<_> = signers.iter().take(3).collect(); + println!(">>> initial_authorities{:?}", initial_authorities); + setup_community_wallet_caged(comm_wallet_pk.clone(), comm_wallet_addr.clone(), &initial_authorities, 2, config_path.clone(), api_endpoint.clone()).await; - // Create initial proposal - let private_key_of_first_signer = signers[1] + // 4. The first authority propose a new community wallet admin and 3 signitures + let new_admin_address = addresses[3]; + let new_admin_pk = signers[3] .private_key() .to_encoded_string() .expect("cannot decode pri key"); + let private_key_of_first_signer = signers[0] + .private_key() + .to_encoded_string() + .expect("cannot decode pri key") + .clone(); - // Verify the admins remain unchanged - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); - let no_of_signers_before = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); - - assert_eq!(no_of_signers_before, 3, "The number of signers should be 3"); - - // Propose add admin let cli_add_new_admin_proposal = TxsCli { subcommand: Some(TxsSub::Community(CommunityTxs::GovAdmin(AdminTx { community_wallet: comm_wallet_addr, admin: new_admin_address, drop: Some(true), - n: 2, + n: 3, epochs: Some(10), }))), mnemonic: None, test_private_key: Some(private_key_of_first_signer), chain_id: None, config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), + url: Some(api_endpoint.clone()), tx_profile: None, tx_cost: Some(TxCost::default_baseline_cost()), estimate_only: false, + legacy_address: false }; cli_add_new_admin_proposal.run() - .await - .expect("CLI could not add new admin to community wallet"); + .await + .expect("CLI could not add new admin to community wallet"); // Verify the admins remain unchanged - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) + let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) .await - .expect("Query failed: community wallet init check"); - let no_of_signers_after_proposal = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); + .expect("Query failed: community wallet authorities check"); - assert_eq!(no_of_signers_after_proposal, 3, "The number of signers should be 3"); + let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities_queried.len(), 3, "There should be 3 authorities"); - // Get second signer private key - let private_key_of_second_signer = signers[2] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); + let authorities_addresses: Vec = initial_authorities.iter().map(|a| a.address()).collect(); + println!(">>> authorities_queried{:?}", authorities_queried); + for i in 0..3 { + let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(authority_str, authorities_addresses[i].to_string(), "Authority should be the same"); + } - // Singer 2 verify new admin - let cli_add_new_admin_proposal = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovAdmin(AdminTx { - community_wallet: comm_wallet_addr, - admin: new_admin_address, - drop: Some(true), - n: 2, - epochs: Some(10), - }))), - mnemonic: None, - test_private_key: Some(private_key_of_second_signer), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; + // 5. All the other authorities vote to add the new admin and change threshold to 3 + for j in 1..3 { + let private_key_of_signer = initial_authorities[j] + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); + let cli_add_new_admin_proposal = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovAdmin(AdminTx { + community_wallet: comm_wallet_addr, + admin: new_admin_address, + drop: Some(true), + n: 3, + epochs: Some(10), + }))), + mnemonic: None, + test_private_key: Some(private_key_of_signer), + chain_id: None, + config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), + url: Some(api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false + }; - cli_add_new_admin_proposal.run() - .await - .expect("CLI could not add new admin to community wallet"); + cli_add_new_admin_proposal.run() + .await + .expect("CLI could not add new admin to community wallet"); + } - // Verify the admins have dropped - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) + // Verify the admins remain unchanged + let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) .await - .expect("Query failed: community wallet init check"); - let no_of_signers_after_second_proposal = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); + .expect("Query failed: community wallet authorities check"); - assert_eq!(no_of_signers_after_second_proposal, 4, "The number of signers should be 4"); + let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities_queried.len(), 3, "There should be 3 authorities"); + + let authorities_addresses: Vec = initial_authorities.iter().map(|a| a.address()).collect(); + for i in 0..3 { + let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(authority_str, authorities_addresses[i].to_string(), "Authority should be the same"); + } + + // 6. New admin claim the offer + run_cli_claim_offer(new_admin_pk, comm_wallet_addr.clone(), api_endpoint.clone(), config_path.clone()).await; + + // 7. Validate the new admin was added + let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet authorities check"); + + let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities_queried.len(), 4, "There should be 3 authorities"); + + let new_authorities_addresses: Vec = signers + .iter() + .take(4) + .map(| a | a.address()) + .collect(); + for i in 0..4 { + let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(new_authorities_addresses[i].to_string(), authority_str, "Authority should be the same"); + } + + // Verify the number of signitures have changed to 3 + let query_res = query_view::get_view( + &client, + "0x1::multi_action::get_threshold", + None, + Some(comm_wallet_addr.clone().to_string()) + ) + .await + .expect("Query failed: community wallet authorities check"); + + let query_ret = query_res.as_array().unwrap(); + assert_eq!(query_ret[0], "3", "There should be 3 signitures"); + assert_eq!(query_ret[1], "4", "There should be 3 signers"); Ok(()) } +/* // TODO: apply once we have a method to progress epochs // Remove an admin #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -1451,12 +1477,13 @@ async fn cancel_community_wallet_payment() -> Result<(), anyhow::Error> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn liquidate_community_wallet() { } +*/ -UTILITY // +// UTILITY // -async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, AccountAddress) { +async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, String, AccountAddress) { let dir = diem_temppath::TempPath::new(); - let mut s = LibraSmoke::new(Some(5)) + let mut s = LibraSmoke::new(Some(5), None) .await .expect("Could not start libra smoke"); @@ -1483,6 +1510,7 @@ async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, AccountAd tx_profile: None, tx_cost: Some(TxCost::default_baseline_cost()), estimate_only: false, + legacy_address: false, }; cli_transfer @@ -1498,6 +1526,148 @@ async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, AccountAd .expect("no first validator") .to_owned(); let comm_wallet_addr = first_node.peer_id(); + let comm_wallet_pk = first_node.account_private_key().as_ref().unwrap().private_key().to_encoded_string().expect("cannot decode pri key"); + + (s, dir, account_address_wrapped, comm_wallet_pk, comm_wallet_addr) +} + +async fn run_cli_transfer( + to_account: AccountAddress, + amount: f64, + private_key: String, + api_endpoint: Url, + config_path: PathBuf, +) { + // Build the CLI command + let cli_transfer = TxsCli { + subcommand: Some(Transfer { + to_account, + amount, + }), + mnemonic: None, + test_private_key: Some(private_key), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + // Execute the transfer + cli_transfer + .run() + .await + .expect(&format!("CLI could not transfer funds to account {}", to_account.to_string())); +} + +async fn run_cli_community_init( + donor_private_key: String, + auhtorities: Vec, + num_signers: u64, + api_endpoint: Url, + config_path: PathBuf, +) { + // Build the CLI command + let cli_set_community_wallet = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovInit(InitTx { + admins: auhtorities, + num_signers: num_signers, + }))), + mnemonic: None, + test_private_key: Some(donor_private_key), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + // Execute the transaction + cli_set_community_wallet.run() + .await + .expect("CLI could not create community wallet"); +} - (s, dir, account_address_wrapped, comm_wallet_addr) +async fn run_cli_claim_offer( + signer_pk: String, + community_address: AccountAddress, + api_endpoint: Url, + config_path: PathBuf +) { + let cli_claim_offer = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovClaim(ClaimTx { + community_wallet: community_address, + }))), + mnemonic: None, + test_private_key: Some(signer_pk), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_claim_offer.run() + .await + .expect("CLI could not claim offer"); +} + +async fn run_cli_community_cage( + donor_private_key: String, + num_signers: u64, + api_endpoint: Url, + config_path: PathBuf +) { + let cli_finalize_cage = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovCage(CageTx { + num_signers: num_signers, + }))), + mnemonic: None, + test_private_key: Some(donor_private_key), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_finalize_cage.run() + .await + .expect("CLI could not finalize and cage community wallet"); +} + +async fn run_cli_community_propose_offer( + donor_private_key: String, + authorities: Vec, + num_signers: u64, + api_endpoint: Url, + config_path: PathBuf +) { + let cli_propose_offer = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovOffer(OfferTx { + admins: authorities, + num_signers: num_signers, + }))), + mnemonic: None, + test_private_key: Some(donor_private_key), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + cli_propose_offer.run() + .await + .expect("CLI could not propose offer"); } diff --git a/tools/txs/tests/cw_temp.rs b/tools/txs/tests/cw_temp.rs deleted file mode 100644 index aba2ddd8f..000000000 --- a/tools/txs/tests/cw_temp.rs +++ /dev/null @@ -1,412 +0,0 @@ -use std::path::PathBuf; - -use diem_crypto::ValidCryptoMaterialStringExt; -use diem_types::account_address::AccountAddress; -use diem_temppath::TempPath; -use libra_query::query_view; -use libra_smoke_tests::{configure_validator, libra_smoke::LibraSmoke}; -use libra_txs::txs_cli::{TxsCli, TxsSub, TxsSub::Transfer}; -use libra_txs::txs_cli_community::{ - CommunityTxs, InitTx, ClaimTx, CageTx, OfferTx -}; -use libra_types::legacy_types::app_cfg::TxCost; -use url::Url; - -// Happy day: create a v7 community wallet -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn create_community_wallet() -> Result<(), anyhow::Error> { - let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; - let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); - - // SETUP ADMIN SIGNERS - // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. - // 2. Transfer funds to the newly created signer accounts to enable their transactional capabilities. - - // SETUP COMMUNITY WALLET - // 3. Prepare a new admin account but do not immediately use it within the community wallet. - // 4. Initialize a community wallet offering the first three of the newly funded accounts as its admins. - // 5. Update the community wallet with a new admin account. - - // SETUP ADMIN SIGNERS // - // 1. Generate and fund 5 new accounts - let (signers, signer_addresses) = s.create_accounts(5).await?; - - // Ensure there's a one-to-one correspondence between signers and private keys - if signer_addresses.len() != s.validator_private_keys.len() { - panic!("The number of signer addresses does not match the number of validator private keys."); - } - - // 2. Transfer funds to the newly created signer accounts - for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { - let to_account = signer_address.clone(); - - // Transfer funds to ensure the account exists on-chain using the specific validator's private key - run_cli_transfer(to_account, 10.0, validator_private_key.clone(), s.api_endpoint.clone(), config_path.clone()).await; - } - - // SETUP COMMUNITY WALLET // - - // 3. Prepare a new admin account - let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; - let new_admin_address = AccountAddress::from_hex_literal(new_admin) - .expect("Failed to parse account address"); - - // Fund with the last signer to avoid ancestry issues - let private_key_of_fifth_signer = signers[4] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); - - // Transfer funds to ensure the account exists on-chain - run_cli_transfer(new_admin_address, 1.0, private_key_of_fifth_signer.clone(), s.api_endpoint.clone(), config_path.clone()).await; - - // Get 3 signers to be admins - let first_three_signer_addresses: Vec = signer_addresses - .clone() - .into_iter() - .take(3) - .collect(); - - // 4. Initialize the community wallet - let donor_private_key = s.encoded_pri_key.clone(); - run_cli_community_init(donor_private_key.clone(), first_three_signer_addresses.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; - - // Verify if the account is not a community wallet yet - let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); - - assert!(!is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should not be a community wallet yet"); - - // Check offer proposed - let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_proposed", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet proposed offer"); - - // Assert authorities are the three proposed - let authorities = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities.len(), 3, "There should be 3 authorities"); - for i in 0..3 { - assert_eq!(authorities[i].as_str().unwrap().trim_start_matches("0x"), first_three_signer_addresses[i].to_string(), "Authority should be the same"); - } - - // 5. Admins claim the offer. - for j in 0..3 { - let auth = &signers[j]; - // print private key - let authority_pk = auth.private_key().to_encoded_string().expect("cannot decode pri key"); - run_cli_claim_offer(authority_pk, comm_wallet_addr.clone(), s.api_endpoint.clone(), config_path.clone()).await; - } - - // Check offer claimed - let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_claimed", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet offer claimed"); - - // Assert authorities are the three proposed - let authorities = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities.len(), 3, "There should be 3 authorities"); - for i in 0..3 { - assert_eq!(authorities[i].as_str().unwrap().trim_start_matches("0x"), first_three_signer_addresses[i].to_string(), "Authority should be the same"); - } - - // 6. Donor finalize and cage the community wallet - run_cli_community_cage(donor_private_key.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; - - // Ensure the account is now a community wallet - let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); - - assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); - - // Ensure authorities are the three proposed - let authrotities_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet authorities check"); - - let authorities = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities.len(), 3, "There should be 3 authorities"); - for i in 0..3 { - assert_eq!(authorities[i].as_str().unwrap().trim_start_matches("0x"), first_three_signer_addresses[i].to_string(), "Authority should be the same"); - } - - Ok(()) -} - -// Happy day: update community wallet offer before cage -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn update_community_wallet_offer() -> Result<(), anyhow::Error> { - let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; - let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); - - // SETUP ADMIN SIGNERS - // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. - // 2. Transfer funds to the newly created signer accounts to enable their transactional capabilities. - - // SETUP COMMUNITY WALLET - // 3. Prepare a new admin account but do not immediately use it within the community wallet. - // 4. Initialize a community wallet offering the first three of the newly funded accounts as its admins. - // 5. Admins claim the offer. - // 6. Donor finalize and cage the community wallet to ensure its independence and security. - - // SETUP ADMIN SIGNERS // - // 1. Generate and fund 5 new accounts - let (signers, signer_addresses) = s.create_accounts(5).await?; - - // Ensure there's a one-to-one correspondence between signers and private keys - if signer_addresses.len() != s.validator_private_keys.len() { - panic!("The number of signer addresses does not match the number of validator private keys."); - } - - // 2. Transfer funds to the newly created signer accounts - for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { - let to_account = signer_address.clone(); - - // Transfer funds to ensure the account exists on-chain using the specific validator's private key - run_cli_transfer(to_account, 10.0, validator_private_key.clone(), s.api_endpoint.clone(), config_path.clone()).await; - } - - // SETUP COMMUNITY WALLET // - - // 3. Prepare a new admin account - let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; - let new_admin_address = AccountAddress::from_hex_literal(new_admin) - .expect("Failed to parse account address"); - - // Fund with the last signer to avoid ancestry issues - let private_key_of_fifth_signer = signers[4] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); - - // Transfer funds to ensure the account exists on-chain - run_cli_transfer(new_admin_address, 1.0, private_key_of_fifth_signer.clone(), s.api_endpoint.clone(), config_path.clone()).await; - - // Get 3 signers to be admins - let mut authorities: Vec = signer_addresses - .clone() - .into_iter() - .take(3) - .collect(); - - // 4. Initialize the community wallet - let donor_private_key = s.encoded_pri_key.clone(); - run_cli_community_init(donor_private_key.clone(), authorities.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; - - // 5. Update the community wallet with a new admin account. - - // Add forth signer as admin - let forth_signer_address = signer_addresses[3].clone(); - authorities.push(forth_signer_address); - run_cli_community_propose_offer(donor_private_key.clone(), authorities.clone(), 4, s.api_endpoint.clone(), config_path.clone()).await; - - // Check offer proposed - let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_proposed", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet proposed offer"); - - // Assert authorities are the three proposed - let proposed = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); - println!("{:?}", authorities); - println!("{:?}", proposed); - assert_eq!(proposed.len(), 4, "There should be 4 authorities"); - for i in 0..4 { - assert_eq!(proposed[i].as_str().unwrap().trim_start_matches("0x"), authorities[i].to_string(), "Authority should be the same"); - } - - Ok(()) -} - - -// UTILITY // - -async fn run_cli_transfer( - to_account: AccountAddress, - amount: f64, - private_key: String, - api_endpoint: Url, - config_path: PathBuf, -) { - // Build the CLI command - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account, - amount, - }), - mnemonic: None, - test_private_key: Some(private_key), - chain_id: None, - config_path: Some(config_path), - url: Some(api_endpoint), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - // Execute the transfer - cli_transfer - .run() - .await - .expect(&format!("CLI could not transfer funds to account {}", to_account.to_string())); -} - -async fn run_cli_community_init( - donor_private_key: String, - auhtorities: Vec, - num_signers: u64, - api_endpoint: Url, - config_path: PathBuf, -) { - // Build the CLI command - let cli_set_community_wallet = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovInit(InitTx { - admins: auhtorities, - num_signers: num_signers, - }))), - mnemonic: None, - test_private_key: Some(donor_private_key), - chain_id: None, - config_path: Some(config_path), - url: Some(api_endpoint), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - // Execute the transaction - cli_set_community_wallet.run() - .await - .expect("CLI could not create community wallet"); -} - -async fn run_cli_claim_offer( - signer_pk: String, - community_address: AccountAddress, - api_endpoint: Url, - config_path: PathBuf -) { - let cli_claim_offer = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovClaim(ClaimTx { - community_wallet: community_address, - }))), - mnemonic: None, - test_private_key: Some(signer_pk), - chain_id: None, - config_path: Some(config_path), - url: Some(api_endpoint), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - cli_claim_offer.run() - .await - .expect("CLI could not claim offer"); -} - -async fn run_cli_community_cage( - donor_private_key: String, - num_signers: u64, - api_endpoint: Url, - config_path: PathBuf -) { - let cli_finalize_cage = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovCage(CageTx { - num_signers: num_signers, - }))), - mnemonic: None, - test_private_key: Some(donor_private_key), - chain_id: None, - config_path: Some(config_path), - url: Some(api_endpoint), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - cli_finalize_cage.run() - .await - .expect("CLI could not finalize and cage community wallet"); -} - -async fn run_cli_community_propose_offer( - donor_private_key: String, - authorities: Vec, - num_signers: u64, - api_endpoint: Url, - config_path: PathBuf -) { - let cli_propose_offer = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovOffer(OfferTx { - admins: authorities, - num_signers: num_signers, - }))), - mnemonic: None, - test_private_key: Some(donor_private_key), - chain_id: None, - config_path: Some(config_path), - url: Some(api_endpoint), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - cli_propose_offer.run() - .await - .expect("CLI could not propose offer"); -} - -async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, AccountAddress) { - let dir = diem_temppath::TempPath::new(); - let mut s = LibraSmoke::new(Some(5), None) - .await - .expect("Could not start libra smoke"); - - configure_validator::init_val_config_files(&mut s.swarm, 0, dir.path().to_owned()) - .await - .expect("Could not initialize validator config"); - - let account_address = "0x029633a96b0c0e81cc26cf2baefdbd479dab7161fbd066ca3be850012342cdee"; - - let account_address_wrapped = - AccountAddress::from_hex_literal(account_address).expect("Failed to parse account address"); - - // Transfer funds to ensure the account exists on-chain - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account: account_address_wrapped, - amount: 100.0, - }), - mnemonic: None, - test_private_key: Some(s.encoded_pri_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - legacy_address: false, - }; - - cli_transfer - .run() - .await - .expect("CLI could not transfer funds to the new account"); - - // get the address of the first node, the private key that was used to create the comm wallet - let first_node = s - .swarm - .validators() - .next() - .expect("no first validator") - .to_owned(); - let comm_wallet_addr = first_node.peer_id(); - - (s, dir, account_address_wrapped, comm_wallet_addr) -} From a0a0f1e641b7526d0630eef9614058ae3f9b2171 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:18:31 -0300 Subject: [PATCH 47/68] fix threshold bug to remove admin from cw + add test to vote removal of admin from cw --- .../ol_sources/community_wallet_init.move | 22 +- framework/releases/head.mrb | Bin 863026 -> 863030 bytes tools/txs/tests/community_wallet.rs | 400 +++++------------- 3 files changed, 109 insertions(+), 313 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/community_wallet_init.move b/framework/libra-framework/sources/ol_sources/community_wallet_init.move index f1eb5c44c..c0e76293b 100644 --- a/framework/libra-framework/sources/ol_sources/community_wallet_init.move +++ b/framework/libra-framework/sources/ol_sources/community_wallet_init.move @@ -20,9 +20,9 @@ module ol_framework::community_wallet_init { #[test_only] friend ol_framework::test_donor_voice; - /// not authorized to operate on this account + /// Not authorized to operate on this account const ENOT_AUTHORIZED: u64 = 1; - /// does not meet criteria for community wallet + /// Does not meet criteria for community wallet const ENOT_QUALIFY_COMMUNITY_WALLET: u64 = 2; /// Recipient does not have a slow wallet const EPAYEE_NOT_SLOW_WALLET: u64 = 8; @@ -30,17 +30,17 @@ module ol_framework::community_wallet_init { const ENOT_DONOR_VOICE: u64 = 3; /// This account needs a multisig enabled const ENOT_MULTISIG: u64 = 4; - /// config has few authorities on multisig + /// Config has few authorities on multisig const ETOO_FEW_AUTH: u64 = 9; - /// config has too few signatures required for each proposal to pass + /// Config has too few signatures required for each proposal to pass const ESIG_THRESHOLD_CONFIG: u64 = 5; /// The multisig threshold is not better than MINIMUM_SIGS/MINIMUM_AUTH const ESIG_THRESHOLD_RATIO: u64 = 6; /// Signers may be sybil const ESIGNERS_SYBIL: u64 = 7; - /// does not liquidate to match index + /// Does not liquidate to match index const ENOT_MATCH_INDEX_LIQ: u64 = 8; - /// does not have the community wallet flag + /// Does not have the community wallet flag const ENO_CW_FLAG: u64 = 9; // STATICS @@ -59,9 +59,9 @@ module ol_framework::community_wallet_init { //////// MULTISIG TX HELPERS //////// // Helper to initialize: - // - the PaymentMultiAction + // - the PaymentMultiAction // - offer authorities to the multisig after confirming that the signers are not related family - // These transactions can be sent directly to donor_voice, but this is a helper to make it easier to initialize + // These transactions can be sent directly to donor_voice, but this is a helper to make it easier to initialize // the multisig with the ancestry requirements. public entry fun init_community( sig: &signer, @@ -75,7 +75,7 @@ module ol_framework::community_wallet_init { donor_voice_txs::set_liquidate_to_match_index(sig, true); }; match_index::opt_into_match_index(sig); - + propose_offer(sig, initial_authorities, check_threshold); } @@ -113,7 +113,7 @@ module ol_framework::community_wallet_init { multi_action::finalize_and_cage(sig, num_signers); community_wallet::set_comm_wallet(sig); - + assert!(donor_voice_txs::is_liquidate_to_match_index(addr), error::invalid_argument(ENOT_MATCH_INDEX_LIQ)); assert!(multisig_thresh(addr), error::invalid_argument(ESIG_THRESHOLD_RATIO)); assert!(!multisig_common_ancestry(addr), error::invalid_argument(ESIGNERS_SYBIL)); @@ -212,7 +212,7 @@ module ol_framework::community_wallet_init { // Verify the signers will not fall below the threshold the signers will fall below threshold if (!is_add_operation) { - assert!((vector::length(¤t_signers) - 1) > MINIMUM_AUTH, error::invalid_argument(ESIG_THRESHOLD_CONFIG)); + assert!((vector::length(¤t_signers) - 1) >= MINIMUM_AUTH, error::invalid_argument(ESIG_THRESHOLD_CONFIG)); }; multi_action::propose_governance( diff --git a/framework/releases/head.mrb b/framework/releases/head.mrb index e8434d01659595ad3cfcf0408dfb7bdb71d7ceec..a6f800d99f9cde34e6cd1277c7199a3111515917 100644 GIT binary patch delta 5443 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�b&9!pMs#NQAStV5^f-x-yiiOx*ELY9f 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 diff --git a/tools/txs/tests/community_wallet.rs b/tools/txs/tests/community_wallet.rs index 588883f16..474003f1c 100644 --- a/tools/txs/tests/community_wallet.rs +++ b/tools/txs/tests/community_wallet.rs @@ -678,7 +678,7 @@ async fn setup_community_wallet_caged(donor_pk: String, donor_address: AccountAd run_cli_community_init(donor_pk.clone(), authorities_addresses, num_signitures, api_endpoint.clone(), config_path.clone()).await; // 2. Admins claim the offer. - for j in 0..3 { + for j in 0..authorities.len() { let auth = &authorities[j]; // print private key let authority_pk = auth.private_key().to_encoded_string().expect("cannot decode pri key"); @@ -686,7 +686,7 @@ async fn setup_community_wallet_caged(donor_pk: String, donor_address: AccountAd } // 3. Donor finalize and cage the community wallet - run_cli_community_cage(donor_pk, 3, api_endpoint, config_path).await; + run_cli_community_cage(donor_pk, num_signitures, api_endpoint, config_path).await; } // Add an admin @@ -709,8 +709,7 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { // 3. Setup community wallet caged with 3 authorities and 2 signitures let initial_authorities: Vec<_> = signers.iter().take(3).collect(); - println!(">>> initial_authorities{:?}", initial_authorities); - setup_community_wallet_caged(comm_wallet_pk.clone(), comm_wallet_addr.clone(), &initial_authorities, 2, config_path.clone(), api_endpoint.clone()).await; + setup_community_wallet_caged(comm_wallet_pk.clone(), comm_wallet_addr.clone(), &initial_authorities, 2, config_path.clone(), api_endpoint.clone()).await; // 4. The first authority propose a new community wallet admin and 3 signitures let new_admin_address = addresses[3]; @@ -756,7 +755,6 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { assert_eq!(authorities_queried.len(), 3, "There should be 3 authorities"); let authorities_addresses: Vec = initial_authorities.iter().map(|a| a.address()).collect(); - println!(">>> authorities_queried{:?}", authorities_queried); for i in 0..3 { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!(authority_str, authorities_addresses[i].to_string(), "Authority should be the same"); @@ -815,7 +813,7 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { .expect("Query failed: community wallet authorities check"); let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities_queried.len(), 4, "There should be 3 authorities"); + assert_eq!(authorities_queried.len(), 4, "There should be 4 authorities"); let new_authorities_addresses: Vec = signers .iter() @@ -844,348 +842,146 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { Ok(()) } -/* -// TODO: apply once we have a method to progress epochs // Remove an admin #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { - let (mut s, dir, _account_address, comm_wallet_addr) = setup_environment().await; - let client = s.client(); - - // SETUP ADMIN SIGNERS - // 1. Generate and fund 5 new accounts from validators to ensure their on-chain presence for signing operations. - // 2. Transfer funds to the newly created signer accounts to enable their transactional capabilities. - - // SETUP COMMUNITY WALLET - // 3. Prepare a new admin account but do not immediately use it within the community wallet. - // 4. Create a community wallet specifying the first three of the newly funded accounts as its admins. - // 5. Confirm the successful creation of the community wallet and its recognition by the system. - // 6. Revoke the original creator account's access to ensure security and independence of the community wallet. - - // ADD NEW ADMIN - // 7. Initiate the process to add a new admin to the community wallet by proposing through an existing admin. - // 8. Validate the addition of the new admin by checking the updated count of admins/signers in the wallet. - - // REMOVE NEW ADMIN - // 9. Start the removal process of the newly added admin through a proposal from an existing admin. - // 10. Complete the admin removal process and verify by checking the updated admins/signers count. +async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { - // SETUP ADMIN SIGNERS // - // We set up 5 new accounts and also fund them from each of the 5 validators - - let (signers, signer_addresses) = s.create_accounts(5).await?; - - // Ensure there's a one-to-one correspondence between signers and private keys - if signer_addresses.len() != s.validator_private_keys.len() { - panic!("The number of signer addresses does not match the number of validator private keys."); - } - - for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { - let to_account = signer_address.clone(); // Adjust this line if necessary + // 1. Setup environment + let (mut smoke, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = setup_environment().await; + let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); + let api_endpoint = smoke.api_endpoint.clone(); + let client = smoke.client(); + // 2. Setup 4 funded accounts + let (signers, addresses) = smoke.create_accounts(4).await?; + for (signer_address, validator_private_key) in addresses.iter().zip(smoke.validator_private_keys.iter()) { + let to_account = signer_address.clone(); // Transfer funds to ensure the account exists on-chain using the specific validator's private key - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account, - amount: 10.0, - }), - mnemonic: None, - test_private_key: Some(validator_private_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - // Execute the transfer - cli_transfer.run() - .await - .expect(&format!("CLI could not transfer funds to account {}", signer_address)); + run_cli_transfer(to_account, 10.0, validator_private_key.clone(), smoke.api_endpoint.clone(), config_path.clone()).await; } - // SETUP COMMUNITY WALLET // - - // Prepare new admin account - let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; - - let new_admin_address = AccountAddress::from_hex_literal(new_admin) - .expect("Failed to parse account address"); - - // Fund with the last signer to avoid ancestry issues - let private_key_of_fifth_signer = signers[4] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); - - // Transfer funds to ensure the account exists on-chain - let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account: new_admin_address, - amount: 1.0, - }), - mnemonic: None, - test_private_key: Some(private_key_of_fifth_signer), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - cli_transfer.run() - .await - .expect("CLI could not transfer funds to the new account"); - - // Get 3 signers to be admins - let first_three_signer_addresses: Vec = signer_addresses - .clone() - .into_iter() - .take(3) - .collect(); - - //create new community wallet - let cli_set_community_wallet = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovInit(InitTx { - admins:first_three_signer_addresses, - migrate_n: None - }))), - mnemonic: None, - test_private_key: Some(s.encoded_pri_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; + // 3. Setup community wallet caged with 4 authorities and 3 signitures + let initial_authorities: Vec<_> = signers.iter().take(4).collect(); + setup_community_wallet_caged(comm_wallet_pk.clone(), comm_wallet_addr.clone(), &initial_authorities, 3, config_path.clone(), api_endpoint.clone()).await; - cli_set_community_wallet.run() - .await - .expect("CLI could not create community wallet"); - - // Verify if the account is a community wallet - let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); - - assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); - - // Remove the ability for the original account to access - let cli_finalize_cage = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::FinalizeAndCage(FinalizeCageTx {}))), - mnemonic: None, - test_private_key: Some(s.encoded_pri_key.clone()), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - cli_finalize_cage.run() + // Verify the cw #admins + let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) .await - .expect("CLI could not finalize and cage the community wallet"); + .expect("Query failed: community wallet authorities check"); - // ADD NEW ADMIN // + let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities_queried.len(), 4, "There should be 4 authorities"); - // Create initial proposal - let private_key_of_first_signer = signers[1] + // 4. The first authority propose to remove the forth admin and set signitures threshold to 2 + let admin_to_remove = addresses[3]; + let private_key_of_first_signer = signers[0] .private_key() .to_encoded_string() - .expect("cannot decode pri key"); - - // Verify the admins remain unchanged - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); - let no_of_signers_before = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); - - assert_eq!(no_of_signers_before, 4, "The number of signers should be 4"); + .expect("cannot decode pri key") + .clone(); - // Propose add admin let cli_add_new_admin_proposal = TxsCli { subcommand: Some(TxsSub::Community(CommunityTxs::GovAdmin(AdminTx { community_wallet: comm_wallet_addr, - admin: new_admin_address, - drop: Some(true), - n: 2, + admin: admin_to_remove, + drop: Some(false), + n: 3, epochs: Some(10), }))), mnemonic: None, test_private_key: Some(private_key_of_first_signer), chain_id: None, config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), + url: Some(api_endpoint.clone()), tx_profile: None, tx_cost: Some(TxCost::default_baseline_cost()), estimate_only: false, + legacy_address: false }; cli_add_new_admin_proposal.run() - .await - .expect("CLI could not add new admin to community wallet"); - - // Verify the admins remain unchanged - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) .await - .expect("Query failed: community wallet init check"); - let no_of_signers_after_proposal = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); - - assert_eq!(no_of_signers_after_proposal, 3, "The number of signers should be 3"); - - // Get second signer private key - let private_key_of_second_signer = signers[2] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); - - // Singer 2 verify new admin - let cli_add_new_admin_proposal = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovAdmin(AdminTx { - community_wallet: comm_wallet_addr, - admin: new_admin_address, - drop: Some(true), - n: 2, - epochs: Some(10), - }))), - mnemonic: None, - test_private_key: Some(private_key_of_second_signer), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; - - cli_add_new_admin_proposal.run() - .await - .expect("CLI could not add new admin to community wallet"); - - // Verify the admins have dropped - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); - let no_of_signers_after_second_proposal = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); - - assert_eq!(no_of_signers_after_second_proposal, 4, "The number of signers should be 4"); - - // REMOVE NEW ADMIN // - - // Create initial proposal - let private_key_of_first_signer = signers[1] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); + .expect("CLI could not add new admin to community wallet"); // Verify the admins remain unchanged - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) + let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) .await - .expect("Query failed: community wallet init check"); - let no_of_signers_before = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); - - assert_eq!(no_of_signers_before, 3, "The number of signers should be 3"); + .expect("Query failed: community wallet authorities check"); - // Propose add admin - let cli_add_new_admin_proposal = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovAdmin(AdminTx { - community_wallet: comm_wallet_addr, - admin: new_admin_address, - drop: Some(false), - n: 2, - epochs: Some(10), - }))), - mnemonic: None, - test_private_key: Some(private_key_of_first_signer), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; + let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities_queried.len(), 4, "There should be 4 authorities"); - cli_add_new_admin_proposal.run() - .await - .expect("CLI could not add new admin to community wallet"); + let authorities_addresses: Vec = initial_authorities.iter().map(|a| a.address()).collect(); + for i in 0..4 { + let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(authority_str, authorities_addresses[i].to_string(), "Authority should be the same"); + } - // Verify the admins remain unchanged - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); - let no_of_signers_after_proposal = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); + // 5. All the other authorities vote to remove the third admin and change threshold to 2 + for j in 1..3 { + let private_key_of_signer = initial_authorities[j] + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); + let cli_add_new_admin_proposal = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovAdmin(AdminTx { + community_wallet: comm_wallet_addr, + admin: admin_to_remove, + drop: Some(false), + n: 3, + epochs: Some(10), + }))), + mnemonic: None, + test_private_key: Some(private_key_of_signer), + chain_id: None, + config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), + url: Some(api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false + }; - //TODO: This should be 4 when we have progressing epochs - assert_eq!(no_of_signers_after_proposal, 3, "The number of signers should be 4"); + cli_add_new_admin_proposal.run() + .await + .expect("CLI could not add new admin to community wallet"); + } - // Get second signer private key - let private_key_of_second_signer = signers[2] - .private_key() - .to_encoded_string() - .expect("cannot decode pri key"); + // 7. Validate the third admin was removed + let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) + .await + .expect("Query failed: community wallet authorities check"); - // Singer 2 verify new admin - let cli_add_new_admin_proposal = TxsCli { - subcommand: Some(TxsSub::Community(CommunityTxs::GovAdmin(AdminTx { - community_wallet: comm_wallet_addr, - admin: new_admin_address, - drop: Some(false), - n: 2, - epochs: Some(10), - }))), - mnemonic: None, - test_private_key: Some(private_key_of_second_signer), - chain_id: None, - config_path: Some(dir.path().to_owned().join("libra-cli-config.yaml")), - url: Some(s.api_endpoint.clone()), - tx_profile: None, - tx_cost: Some(TxCost::default_baseline_cost()), - estimate_only: false, - }; + let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + assert_eq!(authorities_queried.len(), 3, "There should be 3 authorities"); - cli_add_new_admin_proposal.run() - .await - .expect("CLI could not add new admin to community wallet"); + let new_authorities_addresses: Vec = signers + .iter() + .take(3) + .map(| a | a.address()) + .collect(); + for i in 0..3 { + let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix + assert_eq!(new_authorities_addresses[i].to_string(), authority_str, "Authority should be the same"); + } - // Verify the admins have dropped - let comm_wallet_signers = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) + // Verify the number of signitures have changed to 3 + let query_res = query_view::get_view( + &client, + "0x1::multi_action::get_threshold", + None, + Some(comm_wallet_addr.clone().to_string()) + ) .await - .expect("Query failed: community wallet init check"); - let no_of_signers_after_second_proposal = comm_wallet_signers - .as_array() - .and_then(|outer_array| outer_array.get(0)) - .and_then(|inner_array_value| inner_array_value.as_array()) - .map_or(0, |inner_array| inner_array.len()); + .expect("Query failed: community wallet authorities check"); - assert_eq!(no_of_signers_after_second_proposal, 3, "The number of signers should be 3"); + let query_ret = query_res.as_array().unwrap(); + assert_eq!(query_ret[0], "3", "There should be 3 signitures"); + assert_eq!(query_ret[1], "3", "There should be 3 signers"); Ok(()) } - +/* // TODO: apply once we have a method to progress epochs // Veto a payment #[tokio::test(flavor = "multi_thread", worker_threads = 1)] From ea0bef2fc78bde4d3745b65a8ca57ae075f76984 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:25:31 -0300 Subject: [PATCH 48/68] cargo fmt && cargo clippy --- tools/txs/src/txs_cli_community.rs | 6 +- tools/txs/tests/community_wallet.rs | 657 ++++++++++++++++++++-------- 2 files changed, 479 insertions(+), 184 deletions(-) diff --git a/tools/txs/src/txs_cli_community.rs b/tools/txs/src/txs_cli_community.rs index ce2d1b8a7..cb42839a0 100644 --- a/tools/txs/src/txs_cli_community.rs +++ b/tools/txs/src/txs_cli_community.rs @@ -35,7 +35,10 @@ impl CommunityTxs { CommunityTxs::GovInit(init) => match init.run(sender).await { Ok(_) => println!("SUCCESS: community wallet initialized"), Err(e) => { - println!("ERROR: could not initialize Community Wallet, message: {}", e); + println!( + "ERROR: could not initialize Community Wallet, message: {}", + e + ); } }, CommunityTxs::GovOffer(offer) => match offer.run(sender).await { @@ -168,7 +171,6 @@ impl CageTx { } } - #[derive(clap::Args)] pub struct AdminTx { #[clap(short, long)] diff --git a/tools/txs/tests/community_wallet.rs b/tools/txs/tests/community_wallet.rs index 474003f1c..be4f40f70 100644 --- a/tools/txs/tests/community_wallet.rs +++ b/tools/txs/tests/community_wallet.rs @@ -1,15 +1,13 @@ -use std::path::PathBuf; -use diem_sdk::types::LocalAccount; use diem_crypto::ValidCryptoMaterialStringExt; -use diem_types::account_address::AccountAddress; +use diem_sdk::types::LocalAccount; use diem_temppath::TempPath; +use diem_types::account_address::AccountAddress; use libra_query::query_view; use libra_smoke_tests::{configure_validator, libra_smoke::LibraSmoke}; use libra_txs::txs_cli::{TxsCli, TxsSub, TxsSub::Transfer}; -use libra_txs::txs_cli_community::{ - CommunityTxs, InitTx, ClaimTx, CageTx, OfferTx, AdminTx -}; +use libra_txs::txs_cli_community::{AdminTx, CageTx, ClaimTx, CommunityTxs, InitTx, OfferTx}; use libra_types::legacy_types::app_cfg::TxCost; +use std::path::PathBuf; use url::Url; /* @@ -291,7 +289,8 @@ async fn new_community_wallet_cant_transfer() -> Result<(), anyhow::Error> { // Create a v7 community wallet #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn create_community_wallet() -> Result<(), anyhow::Error> { - let (mut s, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = setup_environment().await; + let (mut s, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = + setup_environment().await; let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); // SETUP ADMIN SIGNERS @@ -309,23 +308,34 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { // Ensure there's a one-to-one correspondence between signers and private keys if signer_addresses.len() != s.validator_private_keys.len() { - panic!("The number of signer addresses does not match the number of validator private keys."); + panic!( + "The number of signer addresses does not match the number of validator private keys." + ); } // 2. Transfer funds to the newly created signer accounts - for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { + for (signer_address, validator_private_key) in + signer_addresses.iter().zip(s.validator_private_keys.iter()) + { let to_account = signer_address.clone(); // Transfer funds to ensure the account exists on-chain using the specific validator's private key - run_cli_transfer(to_account, 10.0, validator_private_key.clone(), s.api_endpoint.clone(), config_path.clone()).await; + run_cli_transfer( + to_account, + 10.0, + validator_private_key.clone(), + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; } // SETUP COMMUNITY WALLET // // 3. Prepare a new admin account let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; - let new_admin_address = AccountAddress::from_hex_literal(new_admin) - .expect("Failed to parse account address"); + let new_admin_address = + AccountAddress::from_hex_literal(new_admin).expect("Failed to parse account address"); // Fund with the last signer to avoid ancestry issues let private_key_of_fifth_signer = signers[4] @@ -334,79 +344,158 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { .expect("cannot decode pri key"); // Transfer funds to ensure the account exists on-chain - run_cli_transfer(new_admin_address, 1.0, private_key_of_fifth_signer.clone(), s.api_endpoint.clone(), config_path.clone()).await; + run_cli_transfer( + new_admin_address, + 1.0, + private_key_of_fifth_signer.clone(), + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; // Get 3 signers to be admins - let first_three_signer_addresses: Vec = signer_addresses - .clone() - .into_iter() - .take(3) - .collect(); + let first_three_signer_addresses: Vec = + signer_addresses.clone().into_iter().take(3).collect(); // 4. Initialize the community wallet - run_cli_community_init(comm_wallet_pk.clone(), first_three_signer_addresses.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; + run_cli_community_init( + comm_wallet_pk.clone(), + first_three_signer_addresses.clone(), + 3, + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; // Verify if the account is not a community wallet yet - let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); + let is_comm_wallet_query_res = query_view::get_view( + &s.client(), + "0x1::community_wallet::is_init", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet init check"); - assert!(!is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should not be a community wallet yet"); + assert!( + !is_comm_wallet_query_res.as_array().unwrap()[0] + .as_bool() + .unwrap(), + "Account should not be a community wallet yet" + ); // Check offer proposed - let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_proposed", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet proposed offer"); + let proposed_query_res = query_view::get_view( + &s.client(), + "0x1::multi_action::get_offer_proposed", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet proposed offer"); // Assert authorities are the three proposed - let authorities = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + let authorities = proposed_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); assert_eq!(authorities.len(), 3, "There should be 3 authorities"); for i in 0..3 { let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(authority_str, first_three_signer_addresses[i].to_string(), "Authority should be the same"); + assert_eq!( + authority_str, + first_three_signer_addresses[i].to_string(), + "Authority should be the same" + ); } // 5. Admins claim the offer. for j in 0..3 { let auth = &signers[j]; // print private key - let authority_pk = auth.private_key().to_encoded_string().expect("cannot decode pri key"); - run_cli_claim_offer(authority_pk, comm_wallet_addr.clone(), s.api_endpoint.clone(), config_path.clone()).await; + let authority_pk = auth + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); + run_cli_claim_offer( + authority_pk, + comm_wallet_addr.clone(), + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; } // Check offer claimed - let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_claimed", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet offer claimed"); + let proposed_query_res = query_view::get_view( + &s.client(), + "0x1::multi_action::get_offer_claimed", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet offer claimed"); // Assert authorities are the three proposed - let authorities = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + let authorities = proposed_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); assert_eq!(authorities.len(), 3, "There should be 3 authorities"); for i in 0..3 { let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(authority_str, first_three_signer_addresses[i].to_string(), "Authority should be the same"); + assert_eq!( + authority_str, + first_three_signer_addresses[i].to_string(), + "Authority should be the same" + ); } // 6. Donor finalize and cage the community wallet - run_cli_community_cage(comm_wallet_pk.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; + run_cli_community_cage( + comm_wallet_pk.clone(), + 3, + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; // Ensure the account is now a community wallet - let is_comm_wallet_query_res = query_view::get_view(&s.client(), "0x1::community_wallet::is_init", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet init check"); + let is_comm_wallet_query_res = query_view::get_view( + &s.client(), + "0x1::community_wallet::is_init", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet init check"); - assert!(is_comm_wallet_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should be a community wallet"); + assert!( + is_comm_wallet_query_res.as_array().unwrap()[0] + .as_bool() + .unwrap(), + "Account should be a community wallet" + ); // Ensure authorities are the three proposed - let authrotities_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet authorities check"); + let authrotities_query_res = query_view::get_view( + &s.client(), + "0x1::multi_action::get_authorities", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet authorities check"); - let authorities = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); + let authorities = authrotities_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); assert_eq!(authorities.len(), 3, "There should be 3 authorities"); for i in 0..3 { let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(authority_str, first_three_signer_addresses[i].to_string(), "Authority should be the same"); + assert_eq!( + authority_str, + first_three_signer_addresses[i].to_string(), + "Authority should be the same" + ); } Ok(()) @@ -415,7 +504,8 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { // Happy day: update community wallet offer before cage #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn update_community_wallet_offer() -> Result<(), anyhow::Error> { - let (mut s, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = setup_environment().await; + let (mut s, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = + setup_environment().await; let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); // SETUP ADMIN SIGNERS @@ -434,23 +524,34 @@ async fn update_community_wallet_offer() -> Result<(), anyhow::Error> { // Ensure there's a one-to-one correspondence between signers and private keys if signer_addresses.len() != s.validator_private_keys.len() { - panic!("The number of signer addresses does not match the number of validator private keys."); + panic!( + "The number of signer addresses does not match the number of validator private keys." + ); } // 2. Transfer funds to the newly created signer accounts - for (signer_address, validator_private_key) in signer_addresses.iter().zip(s.validator_private_keys.iter()) { + for (signer_address, validator_private_key) in + signer_addresses.iter().zip(s.validator_private_keys.iter()) + { let to_account = signer_address.clone(); // Transfer funds to ensure the account exists on-chain using the specific validator's private key - run_cli_transfer(to_account, 10.0, validator_private_key.clone(), s.api_endpoint.clone(), config_path.clone()).await; + run_cli_transfer( + to_account, + 10.0, + validator_private_key.clone(), + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; } // SETUP COMMUNITY WALLET // // 3. Prepare a new admin account let new_admin = "0xDCD1AFDFB32A8EB0AADF169ECE2D9BA1552E96FA7D683934F280AC28F29D3611"; - let new_admin_address = AccountAddress::from_hex_literal(new_admin) - .expect("Failed to parse account address"); + let new_admin_address = + AccountAddress::from_hex_literal(new_admin).expect("Failed to parse account address"); // Fund with the last signer to avoid ancestry issues let private_key_of_fifth_signer = signers[4] @@ -459,36 +560,65 @@ async fn update_community_wallet_offer() -> Result<(), anyhow::Error> { .expect("cannot decode pri key"); // Transfer funds to ensure the account exists on-chain - run_cli_transfer(new_admin_address, 1.0, private_key_of_fifth_signer.clone(), s.api_endpoint.clone(), config_path.clone()).await; + run_cli_transfer( + new_admin_address, + 1.0, + private_key_of_fifth_signer.clone(), + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; // Get 3 signers to be admins - let mut authorities: Vec = signer_addresses - .clone() - .into_iter() - .take(3) - .collect(); + let mut authorities: Vec = + signer_addresses.clone().into_iter().take(3).collect(); // 4. Initialize the community wallet - run_cli_community_init(comm_wallet_pk.clone(), authorities.clone(), 3, s.api_endpoint.clone(), config_path.clone()).await; + run_cli_community_init( + comm_wallet_pk.clone(), + authorities.clone(), + 3, + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; // 5. Update the community wallet with a new admin account. // Add forth signer as admin let forth_signer_address = signer_addresses[3].clone(); authorities.push(forth_signer_address); - run_cli_community_propose_offer(comm_wallet_pk.clone(), authorities.clone(), 4, s.api_endpoint.clone(), config_path.clone()).await; + run_cli_community_propose_offer( + comm_wallet_pk.clone(), + authorities.clone(), + 4, + s.api_endpoint.clone(), + config_path.clone(), + ) + .await; // Check offer proposed - let proposed_query_res = query_view::get_view(&s.client(), "0x1::multi_action::get_offer_proposed", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet proposed offer"); + let proposed_query_res = query_view::get_view( + &s.client(), + "0x1::multi_action::get_offer_proposed", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet proposed offer"); // Assert authorities are the three proposed - let proposed = proposed_query_res.as_array().unwrap()[0].as_array().unwrap(); + let proposed = proposed_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); assert_eq!(proposed.len(), 4, "There should be 4 authorities"); for i in 0..4 { let proposed_str = &proposed[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(proposed_str, authorities[i].to_string(), "Authority should be the same"); + assert_eq!( + proposed_str, + authorities[i].to_string(), + "Authority should be the same" + ); } Ok(()) @@ -671,45 +801,44 @@ async fn community_wallet_payment() -> Result<(), anyhow::Error> { */ -async fn setup_community_wallet_caged(donor_pk: String, donor_address: AccountAddress, authorities: &Vec<&LocalAccount>, num_signitures: u64, config_path: PathBuf, api_endpoint: Url) { - - // 1. Initialize the community wallet - let authorities_addresses = authorities.iter().map(|a| a.address()).collect(); - run_cli_community_init(donor_pk.clone(), authorities_addresses, num_signitures, api_endpoint.clone(), config_path.clone()).await; - - // 2. Admins claim the offer. - for j in 0..authorities.len() { - let auth = &authorities[j]; - // print private key - let authority_pk = auth.private_key().to_encoded_string().expect("cannot decode pri key"); - run_cli_claim_offer(authority_pk, donor_address.clone(), api_endpoint.clone(), config_path.clone()).await; - } - - // 3. Donor finalize and cage the community wallet - run_cli_community_cage(donor_pk, num_signitures, api_endpoint, config_path).await; -} - // Add an admin #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { - // 1. Setup environment - let (mut smoke, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = setup_environment().await; + let (mut smoke, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = + setup_environment().await; let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); let api_endpoint = smoke.api_endpoint.clone(); let client = smoke.client(); // 2. Setup 4 funded accounts let (signers, addresses) = smoke.create_accounts(5).await?; - for (signer_address, validator_private_key) in addresses.iter().zip(smoke.validator_private_keys.iter()) { + for (signer_address, validator_private_key) in + addresses.iter().zip(smoke.validator_private_keys.iter()) + { let to_account = signer_address.clone(); // Transfer funds to ensure the account exists on-chain using the specific validator's private key - run_cli_transfer(to_account, 10.0, validator_private_key.clone(), smoke.api_endpoint.clone(), config_path.clone()).await; + run_cli_transfer( + to_account, + 10.0, + validator_private_key.clone(), + smoke.api_endpoint.clone(), + config_path.clone(), + ) + .await; } // 3. Setup community wallet caged with 3 authorities and 2 signitures let initial_authorities: Vec<_> = signers.iter().take(3).collect(); - setup_community_wallet_caged(comm_wallet_pk.clone(), comm_wallet_addr.clone(), &initial_authorities, 2, config_path.clone(), api_endpoint.clone()).await; + setup_community_wallet_caged( + comm_wallet_pk.clone(), + comm_wallet_addr.clone(), + &initial_authorities, + 2, + config_path.clone(), + api_endpoint.clone(), + ) + .await; // 4. The first authority propose a new community wallet admin and 3 signitures let new_admin_address = addresses[3]; @@ -739,25 +868,42 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { tx_profile: None, tx_cost: Some(TxCost::default_baseline_cost()), estimate_only: false, - legacy_address: false + legacy_address: false, }; - cli_add_new_admin_proposal.run() + cli_add_new_admin_proposal + .run() .await .expect("CLI could not add new admin to community wallet"); // Verify the admins remain unchanged - let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet authorities check"); - - let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities_queried.len(), 3, "There should be 3 authorities"); - - let authorities_addresses: Vec = initial_authorities.iter().map(|a| a.address()).collect(); + let authrotities_query_res = query_view::get_view( + &client, + "0x1::multi_action::get_authorities", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet authorities check"); + + let authorities_queried = authrotities_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); + assert_eq!( + authorities_queried.len(), + 3, + "There should be 3 authorities" + ); + + let authorities_addresses: Vec = + initial_authorities.iter().map(|a| a.address()).collect(); for i in 0..3 { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(authority_str, authorities_addresses[i].to_string(), "Authority should be the same"); + assert_eq!( + authority_str, + authorities_addresses[i].to_string(), + "Authority should be the same" + ); } // 5. All the other authorities vote to add the new admin and change threshold to 3 @@ -782,47 +928,82 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { tx_profile: None, tx_cost: Some(TxCost::default_baseline_cost()), estimate_only: false, - legacy_address: false + legacy_address: false, }; - cli_add_new_admin_proposal.run() + cli_add_new_admin_proposal + .run() .await .expect("CLI could not add new admin to community wallet"); } // Verify the admins remain unchanged - let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet authorities check"); - - let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities_queried.len(), 3, "There should be 3 authorities"); - - let authorities_addresses: Vec = initial_authorities.iter().map(|a| a.address()).collect(); + let authrotities_query_res = query_view::get_view( + &client, + "0x1::multi_action::get_authorities", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet authorities check"); + + let authorities_queried = authrotities_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); + assert_eq!( + authorities_queried.len(), + 3, + "There should be 3 authorities" + ); + + let authorities_addresses: Vec = + initial_authorities.iter().map(|a| a.address()).collect(); for i in 0..3 { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(authority_str, authorities_addresses[i].to_string(), "Authority should be the same"); + assert_eq!( + authority_str, + authorities_addresses[i].to_string(), + "Authority should be the same" + ); } // 6. New admin claim the offer - run_cli_claim_offer(new_admin_pk, comm_wallet_addr.clone(), api_endpoint.clone(), config_path.clone()).await; + run_cli_claim_offer( + new_admin_pk, + comm_wallet_addr.clone(), + api_endpoint.clone(), + config_path.clone(), + ) + .await; // 7. Validate the new admin was added - let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet authorities check"); - - let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities_queried.len(), 4, "There should be 4 authorities"); - - let new_authorities_addresses: Vec = signers - .iter() - .take(4) - .map(| a | a.address()) - .collect(); + let authrotities_query_res = query_view::get_view( + &client, + "0x1::multi_action::get_authorities", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet authorities check"); + + let authorities_queried = authrotities_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); + assert_eq!( + authorities_queried.len(), + 4, + "There should be 4 authorities" + ); + + let new_authorities_addresses: Vec = + signers.iter().take(4).map(|a| a.address()).collect(); for i in 0..4 { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(new_authorities_addresses[i].to_string(), authority_str, "Authority should be the same"); + assert_eq!( + new_authorities_addresses[i].to_string(), + authority_str, + "Authority should be the same" + ); } // Verify the number of signitures have changed to 3 @@ -830,10 +1011,10 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { &client, "0x1::multi_action::get_threshold", None, - Some(comm_wallet_addr.clone().to_string()) + Some(comm_wallet_addr.clone().to_string()), ) - .await - .expect("Query failed: community wallet authorities check"); + .await + .expect("Query failed: community wallet authorities check"); let query_ret = query_res.as_array().unwrap(); assert_eq!(query_ret[0], "3", "There should be 3 signitures"); @@ -845,32 +1026,60 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { // Remove an admin #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { - // 1. Setup environment - let (mut smoke, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = setup_environment().await; + let (mut smoke, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = + setup_environment().await; let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); let api_endpoint = smoke.api_endpoint.clone(); let client = smoke.client(); // 2. Setup 4 funded accounts let (signers, addresses) = smoke.create_accounts(4).await?; - for (signer_address, validator_private_key) in addresses.iter().zip(smoke.validator_private_keys.iter()) { + for (signer_address, validator_private_key) in + addresses.iter().zip(smoke.validator_private_keys.iter()) + { let to_account = signer_address.clone(); // Transfer funds to ensure the account exists on-chain using the specific validator's private key - run_cli_transfer(to_account, 10.0, validator_private_key.clone(), smoke.api_endpoint.clone(), config_path.clone()).await; + run_cli_transfer( + to_account, + 10.0, + validator_private_key.clone(), + smoke.api_endpoint.clone(), + config_path.clone(), + ) + .await; } // 3. Setup community wallet caged with 4 authorities and 3 signitures let initial_authorities: Vec<_> = signers.iter().take(4).collect(); - setup_community_wallet_caged(comm_wallet_pk.clone(), comm_wallet_addr.clone(), &initial_authorities, 3, config_path.clone(), api_endpoint.clone()).await; + setup_community_wallet_caged( + comm_wallet_pk.clone(), + comm_wallet_addr.clone(), + &initial_authorities, + 3, + config_path.clone(), + api_endpoint.clone(), + ) + .await; // Verify the cw #admins - let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet authorities check"); + let authrotities_query_res = query_view::get_view( + &client, + "0x1::multi_action::get_authorities", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet authorities check"); - let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities_queried.len(), 4, "There should be 4 authorities"); + let authorities_queried = authrotities_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); + assert_eq!( + authorities_queried.len(), + 4, + "There should be 4 authorities" + ); // 4. The first authority propose to remove the forth admin and set signitures threshold to 2 let admin_to_remove = addresses[3]; @@ -896,25 +1105,42 @@ async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { tx_profile: None, tx_cost: Some(TxCost::default_baseline_cost()), estimate_only: false, - legacy_address: false + legacy_address: false, }; - cli_add_new_admin_proposal.run() + cli_add_new_admin_proposal + .run() .await .expect("CLI could not add new admin to community wallet"); // Verify the admins remain unchanged - let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet authorities check"); - - let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities_queried.len(), 4, "There should be 4 authorities"); - - let authorities_addresses: Vec = initial_authorities.iter().map(|a| a.address()).collect(); + let authrotities_query_res = query_view::get_view( + &client, + "0x1::multi_action::get_authorities", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet authorities check"); + + let authorities_queried = authrotities_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); + assert_eq!( + authorities_queried.len(), + 4, + "There should be 4 authorities" + ); + + let authorities_addresses: Vec = + initial_authorities.iter().map(|a| a.address()).collect(); for i in 0..4 { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(authority_str, authorities_addresses[i].to_string(), "Authority should be the same"); + assert_eq!( + authority_str, + authorities_addresses[i].to_string(), + "Authority should be the same" + ); } // 5. All the other authorities vote to remove the third admin and change threshold to 2 @@ -939,30 +1165,43 @@ async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { tx_profile: None, tx_cost: Some(TxCost::default_baseline_cost()), estimate_only: false, - legacy_address: false + legacy_address: false, }; - cli_add_new_admin_proposal.run() + cli_add_new_admin_proposal + .run() .await .expect("CLI could not add new admin to community wallet"); } // 7. Validate the third admin was removed - let authrotities_query_res = query_view::get_view(&client, "0x1::multi_action::get_authorities", None, Some(comm_wallet_addr.clone().to_string())) - .await - .expect("Query failed: community wallet authorities check"); - - let authorities_queried = authrotities_query_res.as_array().unwrap()[0].as_array().unwrap(); - assert_eq!(authorities_queried.len(), 3, "There should be 3 authorities"); - - let new_authorities_addresses: Vec = signers - .iter() - .take(3) - .map(| a | a.address()) - .collect(); + let authrotities_query_res = query_view::get_view( + &client, + "0x1::multi_action::get_authorities", + None, + Some(comm_wallet_addr.clone().to_string()), + ) + .await + .expect("Query failed: community wallet authorities check"); + + let authorities_queried = authrotities_query_res.as_array().unwrap()[0] + .as_array() + .unwrap(); + assert_eq!( + authorities_queried.len(), + 3, + "There should be 3 authorities" + ); + + let new_authorities_addresses: Vec = + signers.iter().take(3).map(|a| a.address()).collect(); for i in 0..3 { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix - assert_eq!(new_authorities_addresses[i].to_string(), authority_str, "Authority should be the same"); + assert_eq!( + new_authorities_addresses[i].to_string(), + authority_str, + "Authority should be the same" + ); } // Verify the number of signitures have changed to 3 @@ -970,10 +1209,10 @@ async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { &client, "0x1::multi_action::get_threshold", None, - Some(comm_wallet_addr.clone().to_string()) + Some(comm_wallet_addr.clone().to_string()), ) - .await - .expect("Query failed: community wallet authorities check"); + .await + .expect("Query failed: community wallet authorities check"); let query_ret = query_res.as_array().unwrap(); assert_eq!(query_ret[0], "3", "There should be 3 signitures"); @@ -1322,9 +1561,21 @@ async fn setup_environment() -> (LibraSmoke, TempPath, AccountAddress, String, A .expect("no first validator") .to_owned(); let comm_wallet_addr = first_node.peer_id(); - let comm_wallet_pk = first_node.account_private_key().as_ref().unwrap().private_key().to_encoded_string().expect("cannot decode pri key"); + let comm_wallet_pk = first_node + .account_private_key() + .as_ref() + .unwrap() + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); - (s, dir, account_address_wrapped, comm_wallet_pk, comm_wallet_addr) + ( + s, + dir, + account_address_wrapped, + comm_wallet_pk, + comm_wallet_addr, + ) } async fn run_cli_transfer( @@ -1336,10 +1587,7 @@ async fn run_cli_transfer( ) { // Build the CLI command let cli_transfer = TxsCli { - subcommand: Some(Transfer { - to_account, - amount, - }), + subcommand: Some(Transfer { to_account, amount }), mnemonic: None, test_private_key: Some(private_key), chain_id: None, @@ -1352,10 +1600,10 @@ async fn run_cli_transfer( }; // Execute the transfer - cli_transfer - .run() - .await - .expect(&format!("CLI could not transfer funds to account {}", to_account.to_string())); + cli_transfer.run().await.expect(&format!( + "CLI could not transfer funds to account {}", + to_account.to_string() + )); } async fn run_cli_community_init( @@ -1383,7 +1631,8 @@ async fn run_cli_community_init( }; // Execute the transaction - cli_set_community_wallet.run() + cli_set_community_wallet + .run() .await .expect("CLI could not create community wallet"); } @@ -1392,7 +1641,7 @@ async fn run_cli_claim_offer( signer_pk: String, community_address: AccountAddress, api_endpoint: Url, - config_path: PathBuf + config_path: PathBuf, ) { let cli_claim_offer = TxsCli { subcommand: Some(TxsSub::Community(CommunityTxs::GovClaim(ClaimTx { @@ -1409,7 +1658,8 @@ async fn run_cli_claim_offer( legacy_address: false, }; - cli_claim_offer.run() + cli_claim_offer + .run() .await .expect("CLI could not claim offer"); } @@ -1418,7 +1668,7 @@ async fn run_cli_community_cage( donor_private_key: String, num_signers: u64, api_endpoint: Url, - config_path: PathBuf + config_path: PathBuf, ) { let cli_finalize_cage = TxsCli { subcommand: Some(TxsSub::Community(CommunityTxs::GovCage(CageTx { @@ -1435,7 +1685,8 @@ async fn run_cli_community_cage( legacy_address: false, }; - cli_finalize_cage.run() + cli_finalize_cage + .run() .await .expect("CLI could not finalize and cage community wallet"); } @@ -1445,7 +1696,7 @@ async fn run_cli_community_propose_offer( authorities: Vec, num_signers: u64, api_endpoint: Url, - config_path: PathBuf + config_path: PathBuf, ) { let cli_propose_offer = TxsCli { subcommand: Some(TxsSub::Community(CommunityTxs::GovOffer(OfferTx { @@ -1463,7 +1714,49 @@ async fn run_cli_community_propose_offer( legacy_address: false, }; - cli_propose_offer.run() + cli_propose_offer + .run() .await .expect("CLI could not propose offer"); } + +// Helper to setup a community wallet caged with a given number of authorities and signitures +async fn setup_community_wallet_caged( + donor_pk: String, + donor_address: AccountAddress, + authorities: &Vec<&LocalAccount>, + num_signitures: u64, + config_path: PathBuf, + api_endpoint: Url, +) { + // 1. Initialize the community wallet + let authorities_addresses = authorities.iter().map(|a| a.address()).collect(); + run_cli_community_init( + donor_pk.clone(), + authorities_addresses, + num_signitures, + api_endpoint.clone(), + config_path.clone(), + ) + .await; + + // 2. Admins claim the offer. + for j in 0..authorities.len() { + let auth = &authorities[j]; + // print private key + let authority_pk = auth + .private_key() + .to_encoded_string() + .expect("cannot decode pri key"); + run_cli_claim_offer( + authority_pk, + donor_address.clone(), + api_endpoint.clone(), + config_path.clone(), + ) + .await; + } + + // 3. Donor finalize and cage the community wallet + run_cli_community_cage(donor_pk, num_signitures, api_endpoint, config_path).await; +} From d545f13263c76ca2837009f30b50c2c5f4c9d277 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 24 Jun 2024 12:44:37 -0300 Subject: [PATCH 49/68] improves cw tests, fix a threshold bug, duplicated error codes, and error codes usage --- .../ol_sources/community_wallet_init.move | 15 +- .../tests/community_wallet.test.move | 196 +++++++----------- 2 files changed, 81 insertions(+), 130 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/community_wallet_init.move b/framework/libra-framework/sources/ol_sources/community_wallet_init.move index c0e76293b..4ba42039f 100644 --- a/framework/libra-framework/sources/ol_sources/community_wallet_init.move +++ b/framework/libra-framework/sources/ol_sources/community_wallet_init.move @@ -30,8 +30,6 @@ module ol_framework::community_wallet_init { const ENOT_DONOR_VOICE: u64 = 3; /// This account needs a multisig enabled const ENOT_MULTISIG: u64 = 4; - /// Config has few authorities on multisig - const ETOO_FEW_AUTH: u64 = 9; /// Config has too few signatures required for each proposal to pass const ESIG_THRESHOLD_CONFIG: u64 = 5; /// The multisig threshold is not better than MINIMUM_SIGS/MINIMUM_AUTH @@ -42,6 +40,10 @@ module ol_framework::community_wallet_init { const ENOT_MATCH_INDEX_LIQ: u64 = 8; /// Does not have the community wallet flag const ENO_CW_FLAG: u64 = 9; + /// Config has few authorities on multisig + const ETOO_FEW_AUTH: u64 = 10; + /// Config has few signatures on multisig + const ETOO_FEW_SIGS: u64 = 11; // STATICS /// minimum n signatures for a transaction @@ -81,7 +83,7 @@ module ol_framework::community_wallet_init { #[view] /// check if the authorities being proposed, and signature threshold would qualify - public fun check_proposed_auths(initial_authorities: vector

, num_signers: u64): bool { + public fun check_proposed_auths(initial_authorities: vector
, num_signatures: u64): bool { // TODO: enforce n/m multi auth such as: // let n = if (len == 3) { 2 } @@ -89,7 +91,7 @@ module ol_framework::community_wallet_init { // (MINIMUM_SIGS * len) / MINIMUM_AUTH // }; - assert!(num_signers >= MINIMUM_SIGS, error::invalid_argument(ESIG_THRESHOLD_CONFIG)); + assert!(num_signatures >= MINIMUM_SIGS, error::invalid_argument(ETOO_FEW_SIGS)); // policy is to have at least m signers as auths on the account. let len = vector::length(&initial_authorities); @@ -191,6 +193,7 @@ module ol_framework::community_wallet_init { multi_action::get_authorities(multisig_address) } + /// TODO: Allow to propose change only on the signature threshold /// Add or remove a signer to/from the multisig, and check if they may be related in the ancestry tree public entry fun change_signer_community_multisig( sig: &signer, @@ -200,7 +203,7 @@ module ol_framework::community_wallet_init { n_of_m: u64, vote_duration_epochs: u64 ) { - assert!(n_of_m >= MINIMUM_SIGS , error::invalid_argument(ETOO_FEW_AUTH)); + assert!(n_of_m >= MINIMUM_SIGS , error::invalid_argument(ETOO_FEW_SIGS)); let current_signers = multi_action::get_authorities(multisig_address); @@ -212,7 +215,7 @@ module ol_framework::community_wallet_init { // Verify the signers will not fall below the threshold the signers will fall below threshold if (!is_add_operation) { - assert!((vector::length(¤t_signers) - 1) >= MINIMUM_AUTH, error::invalid_argument(ESIG_THRESHOLD_CONFIG)); + assert!((vector::length(¤t_signers) - 1) > MINIMUM_AUTH, error::invalid_argument(ETOO_FEW_AUTH)); }; multi_action::propose_governance( diff --git a/framework/libra-framework/sources/ol_sources/tests/community_wallet.test.move b/framework/libra-framework/sources/ol_sources/tests/community_wallet.test.move index 6eec3717f..75ca852f8 100644 --- a/framework/libra-framework/sources/ol_sources/tests/community_wallet.test.move +++ b/framework/libra-framework/sources/ol_sources/tests/community_wallet.test.move @@ -72,64 +72,34 @@ ol_account::transfer(alice, @0x1000b, 100); } - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c)] - #[expected_failure(abort_code = 65545, location = 0x1::community_wallet_init)] - fun cw_init_below_minimum_sigs(root: &signer, alice: &signer, bob: &signer, carol: &signer) { - // A community wallet by default must be 2/3 multisig. - // This test verifies that the wallet can not be initialized with less signers - mock::genesis_n_vals(root, 4); - mock::ol_initialize_coin_and_fund_vals(root, 1000, true); - - let signers = vector::empty
(); - - // helpers in line to help - vector::push_back(&mut signers, signer::address_of(bob)); - vector::push_back(&mut signers, signer::address_of(carol)); - - community_wallet_init::init_community(alice, signers, 2); - - } - + // Test payment proposal and processing #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, eve = @0x1000e)] - #[expected_failure(abort_code = 65541, location = 0x1::community_wallet_init)] - fun cw_decrease_below_m_authorized_sigs(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer, eve: &signer ) { - // A community wallet by default must be 2/3 multisig. - // This test verifies that the wallet can not be initialized with less signers + fun cw_payment_proposal(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer, eve: &signer ) { mock::genesis_n_vals(root, 5); mock::ol_initialize_coin_and_fund_vals(root, 1000, true); + // initilize accounts let (_, carol_balance_pre) = ol_account::balance(@0x1000c); assert!(carol_balance_pre == 1000, 7357001); - let bob_addr = signer::address_of(bob); let dave_addr = signer::address_of(dave); let eve_addr = signer::address_of(eve); - ancestry::test_fork_migrate(root, bob, vector::singleton(bob_addr)); ancestry::test_fork_migrate(root, dave, vector::singleton(dave_addr)); ancestry::test_fork_migrate(root, eve, vector::singleton(eve_addr)); - let signers = vector::empty
(); - - // helpers in line to help - vector::push_back(&mut signers, signer::address_of(bob)); - vector::push_back(&mut signers, signer::address_of(dave)); - vector::push_back(&mut signers, signer::address_of(eve)); - community_wallet_init::init_community(alice, signers, 2); - - // signers claim the offer + // setup community wallet + community_wallet_init::init_community(alice, vector[bob_addr,dave_addr,eve_addr], 2); multi_action::claim_offer(bob, signer::address_of(alice)); multi_action::claim_offer(dave, signer::address_of(alice)); multi_action::claim_offer(eve, signer::address_of(alice)); - - // fix it by calling multi auth: community_wallet_init::finalize_and_cage(alice, 2); let alice_comm_wallet_addr = signer::address_of(alice); let carols_addr = signer::address_of(carol); - // VERIFY PAYMENTS OPERATE AS EXPECTED + // bob propose payment let uid = donor_voice_txs::test_propose_payment(bob, alice_comm_wallet_addr, carols_addr, 100, b"thanks carol"); let (found, idx, status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(alice_comm_wallet_addr, &uid); assert!(found, 7357004); @@ -140,6 +110,7 @@ // it is not yet scheduled, it's still only a proposal by an admin assert!(!donor_voice_txs::is_scheduled(alice_comm_wallet_addr, &uid), 7357008); + // dave votes the payment and it is approved. let uid = donor_voice_txs::test_propose_payment(dave, alice_comm_wallet_addr, @0x1000c, 100, b"thanks carol"); let (found, idx, status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(alice_comm_wallet_addr, &uid); assert!(found, 7357004); @@ -158,98 +129,15 @@ // process epoch 3 accounts donor_voice_txs::process_donor_voice_accounts(root, 3); + // verify the payment was processed let (_, carol_balance) = ol_account::balance(@0x1000c); assert!(carol_balance > carol_balance_pre, 7357005); assert!(carol_balance == 1100, 7357006); - - // remove a signer and decrease to 2 and verify the community wallet is bricked - // signers must be removed by n signers - community_wallet_init::change_signer_community_multisig(bob, alice_comm_wallet_addr, dave_addr, false, 2, 10); // remove by setting as false - community_wallet_init::change_signer_community_multisig(eve, alice_comm_wallet_addr, dave_addr, false, 2, 10); - - } - - #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d, eve = @0x1000e)] - #[expected_failure(abort_code = 65545, location = 0x1::community_wallet_init)] - fun cw_decrease_below_minimum_n_sigs(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer, eve: &signer ) { - // A community wallet by default must be 2/3 multisig. - // This test verifies that the wallet can not be initialized with less signers - mock::genesis_n_vals(root, 5); - mock::ol_initialize_coin_and_fund_vals(root, 1000, true); - - let (_, carol_balance_pre) = ol_account::balance(@0x1000c); - assert!(carol_balance_pre == 1000, 7357001); - - let bob_addr = signer::address_of(bob); - let dave_addr = signer::address_of(dave); - let eve_addr = signer::address_of(eve); - - ancestry::test_fork_migrate(root, bob, vector::singleton(bob_addr)); - ancestry::test_fork_migrate(root, dave, vector::singleton(dave_addr)); - ancestry::test_fork_migrate(root, eve, vector::singleton(eve_addr)); - - let signers = vector::empty
(); - - // helpers in line to help - vector::push_back(&mut signers, signer::address_of(bob)); - vector::push_back(&mut signers, signer::address_of(dave)); - vector::push_back(&mut signers, signer::address_of(eve)); - community_wallet_init::init_community(alice, signers, 2); - - // signers claim the offer - multi_action::claim_offer(bob, signer::address_of(alice)); - multi_action::claim_offer(dave, signer::address_of(alice)); - multi_action::claim_offer(eve, signer::address_of(alice)); - - // fix it by calling multi auth: - community_wallet_init::finalize_and_cage(alice, 2); - - let alice_comm_wallet_addr = signer::address_of(alice); - let carols_addr = signer::address_of(carol); - - - // VERIFY PAYMENTS OPERATE AS EXPECTED - let uid = donor_voice_txs::test_propose_payment(bob, alice_comm_wallet_addr, carols_addr, 100, b"thanks carol"); - let (found, idx, status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(alice_comm_wallet_addr, &uid); - assert!(found, 7357004); - assert!(idx == 0, 7357005); - assert!(status_enum == 1, 7357006); - assert!(!completed, 7357007); - - // it is not yet scheduled, it's still only a proposal by an admin - assert!(!donor_voice_txs::is_scheduled(alice_comm_wallet_addr, &uid), 7357008); - - let uid = donor_voice_txs::test_propose_payment(dave, alice_comm_wallet_addr, @0x1000c, 100, b"thanks carol"); - let (found, idx, status_enum, completed) = donor_voice_txs::get_multisig_proposal_state(alice_comm_wallet_addr, &uid); - assert!(found, 7357004); - assert!(idx == 0, 7357005); - assert!(status_enum == ballot::get_approved_enum(), 7357006); - assert!(completed, 7357007); // now completed - - // confirm it is scheduled - assert!(donor_voice_txs::is_scheduled(alice_comm_wallet_addr, &uid), 7357008); - - // PROCESS THE PAYMENT - // the default timed payment is 3 epochs, we are in epoch 1 - let list = donor_voice_txs::find_by_deadline(alice_comm_wallet_addr, 3); - assert!(vector::contains(&list, &uid), 7357009); - - // process epoch 3 accounts - donor_voice_txs::process_donor_voice_accounts(root, 3); - - let (_, carol_balance) = ol_account::balance(@0x1000c); - assert!(carol_balance > carol_balance_pre, 7357005); - assert!(carol_balance == 1100, 7357006); - - // remove a signer and decrease to 2 and verify the community wallet is bricked - // signers must be removed by n signers - community_wallet_init::change_signer_community_multisig(bob, alice_comm_wallet_addr, dave_addr, false, 1, 10); // remove by setting as false - } // Try to initialize with less than the required signitures #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b)] - #[expected_failure(abort_code = 0x10005, location = 0x1::community_wallet_init)] + #[expected_failure(abort_code = 0x1000B, location = 0x1::community_wallet_init)] fun cw_init_with_less_signitures_than_min(root: &signer, alice: &signer) { // A community wallet by default must be 2/3 multisig. mock::genesis_n_vals(root, 4); @@ -264,7 +152,7 @@ // Try to initialize with less than the required authorities #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] - #[expected_failure(abort_code = 0x10009, location = 0x1::community_wallet_init)] + #[expected_failure(abort_code = 0x1000A, location = 0x1::community_wallet_init)] fun cw_init_with_less_authorities_than_min(root: &signer, alice: &signer, bob: &signer, carol: &signer) { // A community wallet by default must be 2/3 multisig. mock::genesis_n_vals(root, 4); @@ -334,7 +222,7 @@ vector::pop_back(&mut authorities); // remove dave vector::push_back(&mut authorities, signer::address_of(eve)); // add eve - community_wallet_init::propose_offer(alice, authorities, 2); + community_wallet_init::propose_offer(alice, authorities, 2); multi_action::claim_offer(bob, signer::address_of(alice)); multi_action::claim_offer(carol, signer::address_of(alice)); @@ -349,4 +237,64 @@ assert!(vector::contains(&new_authorities, &signer::address_of(carol)), 7357003); assert!(vector::contains(&new_authorities, &signer::address_of(eve)), 7357004); } - } + + // Try to propose offer with less authorities than the minimum + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x1000A, location = 0x1::community_wallet_init)] + fun cw_propose_offer_with_less_authorities_than_min(root: &signer, alice: &signer) { + mock::genesis_n_vals(root, 4); + community_wallet_init::init_community(alice, vector[@0x1000b, @0x1000c, @0x1000d], 2); + community_wallet_init::propose_offer(alice, vector[@0x1000b, @0x1000c], 2); + } + + // Try to propose offer with less signatures than the minimum + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x1000B, location = 0x1::community_wallet_init)] + fun cw_propose_offer_with_less_signatures_than_min(root: &signer, alice: &signer) { + mock::genesis_n_vals(root, 4); + community_wallet_init::init_community(alice, vector[@0x1000b, @0x1000c, @0x1000d], 2); + community_wallet_init::propose_offer(alice, vector[@0x1000b, @0x1000c], 1); + } + + // Try to reduce the number of signatures below the minimum + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x1000B, location = 0x1::community_wallet_init)] + fun cw_decrease_signatures_below_minimum(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + mock::genesis_n_vals(root, 5); + let alice_address = signer::address_of(alice); // community wallet address + + // 1. Initializes a community wallet with 3 authorities and 2 signatures. + let authorities = vector[@0x1000b, @0x1000c, @0x1000d]; + community_wallet_init::init_community(alice, authorities, 2); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::claim_offer(dave, alice_address); + community_wallet_init::finalize_and_cage(alice, 2); + let (num_signatures, _) = multi_action::get_threshold(alice_address); + assert!(num_signatures == 2, 73573001); + + // 2. Try to change the requirement to 1 signature when adding eve + community_wallet_init::change_signer_community_multisig(bob, alice_address, @0x1000e, true, 1, 10); + } + + // Try to reduce the number of authorities below the minimum + #[test(root = @ol_framework, alice = @0x1000a, bob = @0x1000b, carol = @0x1000c, dave = @0x1000d)] + #[expected_failure(abort_code = 0x1000A, location = 0x1::community_wallet_init)] + fun cw_decrease_authorities_below_minimum(root: &signer, alice: &signer, bob: &signer, carol: &signer, dave: &signer) { + mock::genesis_n_vals(root, 5); + let alice_address = signer::address_of(alice); // community wallet address + + // 1. Initializes a community wallet with 3 authorities and 2 signatures. + let authorities = vector[@0x1000b, @0x1000c, @0x1000d]; + community_wallet_init::init_community(alice, authorities, 2); + multi_action::claim_offer(bob, alice_address); + multi_action::claim_offer(carol, alice_address); + multi_action::claim_offer(dave, alice_address); + community_wallet_init::finalize_and_cage(alice, 2); + let (num_signatures, _) = multi_action::get_threshold(alice_address); + assert!(num_signatures == 2, 73573001); + + // 2. Try to remove authorities below the minimum + community_wallet_init::change_signer_community_multisig(bob, alice_address, @0x1000b, false, 2, 10); + } +} From 7f24ea0620252591ec7dc0178fc5cec955a233e7 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:43:11 -0300 Subject: [PATCH 50/68] adds CLI and smoke test for offer migration --- .../ol_sources/vote_lib/multi_action.move | 3 +- framework/releases/head.mrb | Bin 863030 -> 863544 bytes tools/txs/src/txs_cli_community.rs | 47 +++++++++ tools/txs/tests/community_wallet.rs | 97 +++++++++++++++++- 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 2ec2d441a..1a6944988 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -1021,8 +1021,7 @@ module ol_framework::multi_action { // TODO: remove this after offer migration is completed - #[test_only] - public(friend) fun init_gov_deprecated(sig: &signer) { + public(friend) entry fun init_gov_deprecated(sig: &signer) { let multisig_address = signer::address_of(sig); if (!exists(multisig_address)) { diff --git a/framework/releases/head.mrb b/framework/releases/head.mrb index a6f800d99f9cde34e6cd1277c7199a3111515917..7a444063b0f2911e7c9ada79b6028667c9d64169 100644 GIT binary patch delta 31536 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 delta 31118 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 diff --git a/tools/txs/src/txs_cli_community.rs b/tools/txs/src/txs_cli_community.rs index cb42839a0..372f0fbe2 100644 --- a/tools/txs/src/txs_cli_community.rs +++ b/tools/txs/src/txs_cli_community.rs @@ -27,6 +27,10 @@ pub enum CommunityTxs { Batch(BatchTx), /// Donors to Donor Voice addresses can vote to reject transactions Veto(VetoTx), + /// Migrate legacy account to initialize offer structure + Migration(MigrateOfferTx), // TODO remove after migration complete + /// Initilize legacy multi-sig account governance + GovInitDeprectated, // TODO remove after migration complete } impl CommunityTxs { @@ -83,10 +87,33 @@ impl CommunityTxs { println!("ERROR: could not add admin, message: {}", e); } }, + CommunityTxs::Migration(migration) => match migration.run(sender).await { + Ok(_) => {} + Err(e) => { + println!("ERROR: could not migrate, message: {}", e); + } + }, + // for tests only - TODO Remove when migration is finished + CommunityTxs::GovInitDeprectated => match self.run_init_deprecated(sender).await { + Ok(_) => println!("SUCCESS: community wallet initialized"), + Err(e) => { + println!( + "ERROR: could not initialize Community Wallet, message: {}", + e + ); + } + }, } Ok(()) } + + // for tests only - TODO Remove when migration is finished + async fn run_init_deprecated(&self, sender: &mut Sender) -> anyhow::Result<()> { + let payload = libra_stdlib::multi_action_init_gov_deprecated(); + sender.sign_submit_wait(payload).await?; + Ok(()) + } } #[derive(clap::Args)] @@ -468,3 +495,23 @@ impl VetoTx { Ok(()) } } + + +// TODO remove after migration is completed +#[derive(clap::Args)] +pub struct MigrateOfferTx { + #[clap(short, long)] + /// The Community Wallet to propose the offer + pub community_wallet: AccountAddress, +} + +impl MigrateOfferTx { + pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { + let payload = libra_stdlib::multi_action_migration_migrate_offer( + self.community_wallet, + ); + sender.sign_submit_wait(payload).await?; + println!("You have migrated the account to have the Offer structure. You can proceed with the authority offer now."); + Ok(()) + } +} diff --git a/tools/txs/tests/community_wallet.rs b/tools/txs/tests/community_wallet.rs index be4f40f70..79e2bbb5d 100644 --- a/tools/txs/tests/community_wallet.rs +++ b/tools/txs/tests/community_wallet.rs @@ -5,7 +5,7 @@ use diem_types::account_address::AccountAddress; use libra_query::query_view; use libra_smoke_tests::{configure_validator, libra_smoke::LibraSmoke}; use libra_txs::txs_cli::{TxsCli, TxsSub, TxsSub::Transfer}; -use libra_txs::txs_cli_community::{AdminTx, CageTx, ClaimTx, CommunityTxs, InitTx, OfferTx}; +use libra_txs::txs_cli_community::{AdminTx, CageTx, ClaimTx, CommunityTxs, InitTx, OfferTx, MigrateOfferTx}; use libra_types::legacy_types::app_cfg::TxCost; use std::path::PathBuf; use url::Url; @@ -1760,3 +1760,98 @@ async fn setup_community_wallet_caged( // 3. Donor finalize and cage the community wallet run_cli_community_cage(donor_pk, num_signitures, api_endpoint, config_path).await; } + + +// Test Offer migration of a legacy account +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_offer_migration() -> Result<(), anyhow::Error> { + // 1. Setup environment + let (mut smoke, dir, _account_address, comm_wallet_pk, comm_wallet_addr) = + setup_environment().await; + let config_path = dir.path().to_owned().join("libra-cli-config.yaml"); + let api_endpoint = smoke.api_endpoint.clone(); + // let client = smoke.client(); + + // 2. Setup legacy account + let (signers, addresses) = smoke.create_accounts(1).await?; + for (signer_address, validator_private_key) in + addresses.iter().zip(smoke.validator_private_keys.iter()) + { + let to_account = signer_address.clone(); + // Transfer funds to ensure the account exists on-chain using the specific validator's private key + run_cli_transfer( + to_account, + 10.0, + validator_private_key.clone(), + smoke.api_endpoint.clone(), + config_path.clone(), + ) + .await; + } + let community_wallet_pk = signers[0].private_key().to_encoded_string().expect("cannot decode pri key"); + let community_wallet_address = addresses[0].clone(); + + // 3. Initialize deprecated governance + let init_gov_deprecated = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::GovInitDeprectated)), + mnemonic: None, + test_private_key: Some(community_wallet_pk.clone()), + chain_id: None, + config_path: Some(config_path.clone()), + url: Some(api_endpoint.clone()), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + init_gov_deprecated + .run() + .await + .expect("CLI could not propose offer"); + + // Certify the account does not have an offer + let is_offer_query_res = query_view::get_view( + &smoke.client(), "0x1::multi_action::exists_offer", + None, + Some(community_wallet_address.clone().to_string()) + ) + .await + .expect("Query failed: community wallet offer check"); + + assert!(!is_offer_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should not have an offer"); + + // 4. Run offer migration + let offer_migration = TxsCli { + subcommand: Some(TxsSub::Community(CommunityTxs::Migration(MigrateOfferTx { + community_wallet: community_wallet_address, + }))), + mnemonic: None, + test_private_key: Some(community_wallet_pk), + chain_id: None, + config_path: Some(config_path), + url: Some(api_endpoint), + tx_profile: None, + tx_cost: Some(TxCost::default_baseline_cost()), + estimate_only: false, + legacy_address: false, + }; + + offer_migration + .run() + .await + .expect("CLI could not propose offer"); + + // certify the account has an offer + let is_offer_query_res = query_view::get_view( + &smoke.client(), "0x1::multi_action::exists_offer", + None, + Some(community_wallet_address.clone().to_string()) + ) + .await + .expect("Query failed: community wallet offer check"); + + assert!(is_offer_query_res.as_array().unwrap()[0].as_bool().unwrap(), "Account should have an offer"); + + Ok(()) +} From 74c843d7ad345b567bdadae25acfe8da3baea72b Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:34:13 -0300 Subject: [PATCH 51/68] fix threshold check to reduce cw admins --- .../src/libra_framework_sdk_builder.rs | 34 ++++++++++++++++++ .../ol_sources/community_wallet_init.move | 2 +- .../ol_sources/vote_lib/multi_action.move | 2 -- framework/releases/head.mrb | Bin 863544 -> 863549 bytes tools/txs/tests/community_wallet.rs | 25 ++++++------- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/framework/cached-packages/src/libra_framework_sdk_builder.rs b/framework/cached-packages/src/libra_framework_sdk_builder.rs index 828b99af1..77434e1ba 100644 --- a/framework/cached-packages/src/libra_framework_sdk_builder.rs +++ b/framework/cached-packages/src/libra_framework_sdk_builder.rs @@ -154,6 +154,7 @@ pub enum EntryFunctionCall { amount: u64, }, + /// TODO: Allow to propose change only on the signature threshold /// Add or remove a signer to/from the multisig, and check if they may be related in the ancestry tree CommunityWalletInitChangeSignerCommunityMultisig { multisig_address: AccountAddress, @@ -287,6 +288,8 @@ pub enum EntryFunctionCall { multisig_address: AccountAddress, }, + MultiActionInitGovDeprecated {}, + MultiActionMigrationMigrateOffer { multisig_address: AccountAddress, }, @@ -704,6 +707,7 @@ impl EntryFunctionCall { MultiActionClaimOffer { multisig_address } => { multi_action_claim_offer(multisig_address) } + MultiActionInitGovDeprecated {} => multi_action_init_gov_deprecated(), MultiActionMigrationMigrateOffer { multisig_address } => { multi_action_migration_migrate_offer(multisig_address) } @@ -1158,6 +1162,7 @@ pub fn coin_transfer(coin_type: TypeTag, to: AccountAddress, amount: u64) -> Tra )) } +/// TODO: Allow to propose change only on the signature threshold /// Add or remove a signer to/from the multisig, and check if they may be related in the ancestry tree pub fn community_wallet_init_change_signer_community_multisig( multisig_address: AccountAddress, @@ -1609,6 +1614,21 @@ pub fn multi_action_claim_offer(multisig_address: AccountAddress) -> Transaction )) } +pub fn multi_action_init_gov_deprecated() -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("multi_action").to_owned(), + ), + ident_str!("init_gov_deprecated").to_owned(), + vec![], + vec![], + )) +} + pub fn multi_action_migration_migrate_offer( multisig_address: AccountAddress, ) -> TransactionPayload { @@ -2777,6 +2797,16 @@ mod decoder { } } + pub fn multi_action_init_gov_deprecated( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(_script) = payload { + Some(EntryFunctionCall::MultiActionInitGovDeprecated {}) + } else { + None + } + } + pub fn multi_action_migration_migrate_offer( payload: &TransactionPayload, ) -> Option { @@ -3350,6 +3380,10 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy MINIMUM_AUTH, error::invalid_argument(ETOO_FEW_AUTH)); + assert!((vector::length(¤t_signers) - 1) >= MINIMUM_AUTH, error::invalid_argument(ETOO_FEW_AUTH)); }; multi_action::propose_governance( diff --git a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move index 1a6944988..d55ee3ecc 100644 --- a/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move +++ b/framework/libra-framework/sources/ol_sources/vote_lib/multi_action.move @@ -698,8 +698,6 @@ module ol_framework::multi_action { false } - - fun find_expired(a: & Action): vector{ let epoch = epoch_helper::get_current_epoch(); let b_vec = ballot::get_list_ballots_by_enum(&a.vote, ballot::get_pending_enum()); diff --git a/framework/releases/head.mrb b/framework/releases/head.mrb index 7a444063b0f2911e7c9ada79b6028667c9d64169..b63243772b0f84f612318448d21b2e022d261b20 100644 GIT binary patch delta 21039 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 b+6afz+6jl!+6uSQ+6(WI0VcP?Yz%h5Hq3Ud delta 21034 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%0DV-}EtV8Wygt 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 X+6jl(+6uSV+6(WI0VKD_Yz%h5ZH7UZ diff --git a/tools/txs/tests/community_wallet.rs b/tools/txs/tests/community_wallet.rs index 79e2bbb5d..f1714a372 100644 --- a/tools/txs/tests/community_wallet.rs +++ b/tools/txs/tests/community_wallet.rs @@ -403,7 +403,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( authority_str, - first_three_signer_addresses[i].to_string(), + first_three_signer_addresses[i].to_string().trim_start_matches('0'), "Authority should be the same" ); } @@ -444,7 +444,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( authority_str, - first_three_signer_addresses[i].to_string(), + first_three_signer_addresses[i].to_string().trim_start_matches('0'), "Authority should be the same" ); } @@ -493,7 +493,7 @@ async fn create_community_wallet() -> Result<(), anyhow::Error> { let authority_str = &authorities[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( authority_str, - first_three_signer_addresses[i].to_string(), + first_three_signer_addresses[i].to_string().trim_start_matches('0'), "Authority should be the same" ); } @@ -616,7 +616,7 @@ async fn update_community_wallet_offer() -> Result<(), anyhow::Error> { let proposed_str = &proposed[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( proposed_str, - authorities[i].to_string(), + authorities[i].to_string().trim_start_matches('0'), "Authority should be the same" ); } @@ -901,7 +901,7 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( authority_str, - authorities_addresses[i].to_string(), + authorities_addresses[i].to_string().trim_start_matches('0'), "Authority should be the same" ); } @@ -962,7 +962,7 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( authority_str, - authorities_addresses[i].to_string(), + authorities_addresses[i].to_string().trim_start_matches('0'), "Authority should be the same" ); } @@ -1000,7 +1000,7 @@ async fn add_community_wallet_admin() -> Result<(), anyhow::Error> { for i in 0..4 { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( - new_authorities_addresses[i].to_string(), + new_authorities_addresses[i].to_string().trim_start_matches('0'), authority_str, "Authority should be the same" ); @@ -1094,7 +1094,7 @@ async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { community_wallet: comm_wallet_addr, admin: admin_to_remove, drop: Some(false), - n: 3, + n: 2, epochs: Some(10), }))), mnemonic: None, @@ -1135,10 +1135,11 @@ async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { let authorities_addresses: Vec = initial_authorities.iter().map(|a| a.address()).collect(); for i in 0..4 { + println!("{:?}", authorities_queried[i]); let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( authority_str, - authorities_addresses[i].to_string(), + authorities_addresses[i].to_string().trim_start_matches('0'), "Authority should be the same" ); } @@ -1154,7 +1155,7 @@ async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { community_wallet: comm_wallet_addr, admin: admin_to_remove, drop: Some(false), - n: 3, + n: 2, epochs: Some(10), }))), mnemonic: None, @@ -1198,7 +1199,7 @@ async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { for i in 0..3 { let authority_str = &authorities_queried[i].as_str().unwrap()[2..]; // Remove the "0x" prefix assert_eq!( - new_authorities_addresses[i].to_string(), + new_authorities_addresses[i].to_string().trim_start_matches('0'), authority_str, "Authority should be the same" ); @@ -1215,7 +1216,7 @@ async fn remove_community_wallet_admin() -> Result<(), anyhow::Error> { .expect("Query failed: community wallet authorities check"); let query_ret = query_res.as_array().unwrap(); - assert_eq!(query_ret[0], "3", "There should be 3 signitures"); + assert_eq!(query_ret[0], "2", "There should be 2 signitures"); assert_eq!(query_ret[1], "3", "There should be 3 signers"); Ok(()) From 9f1e842d382919450f79ee47926b1eedadeb9564 Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:47:50 -0300 Subject: [PATCH 52/68] adds offer migration test with twin db --- .cargo/config.toml | 23 ++++- tools/rescue/src/lib.rs | 1 + tools/rescue/src/twin.rs | 22 ++-- tools/rescue/tests/support/mod.rs | 154 ++++++++++++++++++++++++++++ tools/txs/tests/community_wallet.rs | 11 +- types/src/core_types/app_cfg.rs | 18 ++-- 6 files changed, 206 insertions(+), 23 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index b8a5253cc..3d96cb38a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -15,7 +15,18 @@ rustflags = ["--cfg", "tokio_unstable", "-C", "force-frame-pointers=yes", "-C", # TODO(grao): Figure out whether we should enable other cpu features, and whether we should use a different way to configure them rather than list every single one here. [target.x86_64-unknown-linux-gnu] -rustflags = ["--cfg", "tokio_unstable", "-C", "link-arg=-fuse-ld=lld", "-C", "force-frame-pointers=yes", "-C", "force-unwind-tables=yes", "-C", "target-feature=+sse4.2"] +rustflags = [ + "--cfg", + "tokio_unstable", + "-C", + "link-arg=-fuse-ld=lld", + "-C", + "force-frame-pointers=yes", + "-C", + "force-unwind-tables=yes", + "-C", + "target-feature=+sse4.2", +] # 64 bit MSVC [target.x86_64-pc-windows-msvc] @@ -27,5 +38,11 @@ rustflags = [ "-C", "force-unwind-tables=yes", "-C", - "link-arg=/STACK:8000000" # Set stack to 8 MB -] \ No newline at end of file + "link-arg=/STACK:8000000", # Set stack to 8 MB +] + +[target.aarch64-apple-darwin] +rustflags = [ + "-C", + "link-arg=-L/opt/homebrew/opt/gmp", +] diff --git a/tools/rescue/src/lib.rs b/tools/rescue/src/lib.rs index 5a12022af..bbabab808 100644 --- a/tools/rescue/src/lib.rs +++ b/tools/rescue/src/lib.rs @@ -2,3 +2,4 @@ pub mod diem_db_bootstrapper; pub mod rescue_tx; pub mod session_tools; pub mod twin; +pub use twin::{Twin, TwinSetup}; diff --git a/tools/rescue/src/twin.rs b/tools/rescue/src/twin.rs index 90d56f877..358408d02 100644 --- a/tools/rescue/src/twin.rs +++ b/tools/rescue/src/twin.rs @@ -134,7 +134,7 @@ where /// ''' Setup the twin network with a synced db /// ''' #[async_trait] -trait TwinSetup { +pub trait TwinSetup { async fn initialize_marlon_the_val() -> anyhow::Result; fn register_marlon_tx(file: PathBuf) -> anyhow::Result