From fdb45d2e77ad12c4f551c7cce278eddb0b961ef1 Mon Sep 17 00:00:00 2001 From: Manos Liolios Date: Wed, 18 Feb 2026 16:24:23 +0200 Subject: [PATCH] Makes sure approvals are received in the right order, and does not have extra --- packages/pas/sources/requests/request.move | 11 +- packages/pas/tests/e2e.move | 136 ++++++++++++++++++++- 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/packages/pas/sources/requests/request.move b/packages/pas/sources/requests/request.move index d03eae0..5167006 100644 --- a/packages/pas/sources/requests/request.move +++ b/packages/pas/sources/requests/request.move @@ -5,7 +5,11 @@ use sui::vec_set::{Self, VecSet}; #[error(code = 0)] const EInsufficientApprovals: vector = - b"Cannot resolve request: insufficient approvals received."; + b"Cannot resolve request: insufficient or invalid approvals received."; + +#[error(code = 1)] +const EInvalidNumberOfApprovals: vector = + b"Cannot resolve request: Invalid number of approvals received."; /// A base request type. /// Examples: @@ -39,8 +43,9 @@ public(package) fun new(data: K): Request { /// An internal function to resolve a request. public(package) fun resolve(request: Request, required_approvals: VecSet): K { - required_approvals.keys().do_ref!(|approval| { - assert!(request.approvals.contains(approval), EInsufficientApprovals); + assert!(request.approvals.length() == required_approvals.length(), EInvalidNumberOfApprovals); + request.approvals.into_keys().zip_do_ref!(&required_approvals.into_keys(), |a, b| { + assert!(a == b, EInsufficientApprovals); }); let Request { data, .. } = request; data diff --git a/packages/pas/tests/e2e.move b/packages/pas/tests/e2e.move index 665a716..301e604 100644 --- a/packages/pas/tests/e2e.move +++ b/packages/pas/tests/e2e.move @@ -1,9 +1,9 @@ #[test_only, allow(unused_variable, unused_mut_ref, dead_code)] module pas::e2e; -use pas::{rule, transfer_funds, unlock_funds, vault::{Self, Vault}}; -use std::unit_test::{assert_eq, destroy}; -use sui::{balance::{Self, send_funds}, sui::SUI, test_scenario::return_shared}; +use pas::{rule::{Self, RuleCap}, transfer_funds, unlock_funds, vault::{Self, Vault}}; +use std::{type_name, unit_test::{assert_eq, destroy}}; +use sui::{balance::{Self, send_funds}, sui::SUI, test_scenario::return_shared, vec_set}; public struct A has drop {} public struct B has drop {} @@ -302,6 +302,136 @@ fun try_to_create_duplicate_rule() { }); } +#[test] +fun multiple_approvals_required() { + test_tx!(@0x1, |namespace, managed_rule, unmanaged_rule, scenario| { + scenario.next_tx(@0x1); + + let namespace_id = object::id(namespace); + let rule_cap = scenario.take_from_sender>(); + + let mut approvals = vec_set::empty(); + approvals.insert(type_name::with_defining_ids()); + approvals.insert(type_name::with_defining_ids()); + + managed_rule.set_required_approvals(&rule_cap, "transfer_funds", approvals); + + scenario.return_to_sender(rule_cap); + + // create vaults of 0x1 and 0x2 + let vault = vault::create(namespace, @0x1); + + // transfer some funds to both 0x1 and 0x2 + vault.deposit_funds(balance::create_for_testing(100)); + vault.share(); + + scenario.next_tx(@0x1); + + let mut vault = scenario.take_shared_by_id(namespace + .vault_address( + @0x1, + ) + .to_id()); + + let auth = vault::new_auth(scenario.ctx()); + let mut transfer_request = vault.unsafe_transfer_funds( + &auth, + @0x2, + 50, + scenario.ctx(), + ); + + transfer_request.approve(AWitness()); + transfer_request.approve(BWitness()); + transfer_funds::resolve(transfer_request, managed_rule); + + return_shared(vault); + }); +} + +#[test, expected_failure(abort_code = ::pas::request::EInsufficientApprovals)] +fun multiple_approvals_invalid_order_failure() { + test_tx!(@0x1, |namespace, managed_rule, unmanaged_rule, scenario| { + scenario.next_tx(@0x1); + + let namespace_id = object::id(namespace); + let rule_cap = scenario.take_from_sender>(); + + let mut approvals = vec_set::empty(); + approvals.insert(type_name::with_defining_ids()); + approvals.insert(type_name::with_defining_ids()); + + managed_rule.set_required_approvals(&rule_cap, "transfer_funds", approvals); + + scenario.return_to_sender(rule_cap); + + // create vaults of 0x1 and 0x2 + let vault = vault::create(namespace, @0x1); + + // transfer some funds to both 0x1 and 0x2 + vault.deposit_funds(balance::create_for_testing(100)); + vault.share(); + + scenario.next_tx(@0x1); + + let mut vault = scenario.take_shared_by_id(namespace + .vault_address( + @0x1, + ) + .to_id()); + + let auth = vault::new_auth(scenario.ctx()); + let mut transfer_request = vault.unsafe_transfer_funds( + &auth, + @0x2, + 50, + scenario.ctx(), + ); + transfer_request.approve(BWitness()); + transfer_request.approve(AWitness()); + + transfer_funds::resolve(transfer_request, managed_rule); + abort + }); +} + +#[test, expected_failure(abort_code = ::pas::request::EInvalidNumberOfApprovals)] +fun cannot_have_extra_approvals() { + test_tx!(@0x1, |namespace, managed_rule, unmanaged_rule, scenario| { + scenario.next_tx(@0x1); + + let namespace_id = object::id(namespace); + + // create vaults of 0x1 and 0x2 + let vault = vault::create(namespace, @0x1); + + // transfer some funds to both 0x1 and 0x2 + vault.deposit_funds(balance::create_for_testing(100)); + vault.share(); + + scenario.next_tx(@0x1); + + let mut vault = scenario.take_shared_by_id(namespace + .vault_address( + @0x1, + ) + .to_id()); + + let auth = vault::new_auth(scenario.ctx()); + let mut transfer_request = vault.unsafe_transfer_funds( + &auth, + @0x2, + 50, + scenario.ctx(), + ); + transfer_request.approve(BWitness()); + transfer_request.approve(AWitness()); + + transfer_funds::resolve(transfer_request, managed_rule); + abort + }); +} + public fun package_id(): ID { sui::address::from_ascii_bytes(std::type_name::with_defining_ids() .address_string()