Repository: stellar/rs-soroban-env
Pull request #1075 (2023-09-19 18:44:36)

Attach the auth in recording mode to any valid tracker.
### What

Attach the auth in recording mode to any valid tracker.

Also improved the test coverage to cover the case where different objects need to be mapped to the same auth tree, as well as additional coverage for duplicate address auth.

### Why

Currently only the tracker that matches with exact same object is used and otherwise a new tracker is created. This results in recording auth producing unnecessarily disjoint call trees.

This is kind of best effort and might lead to a bit weird edge-case scenarios in semantic sense (e.g. a tracker for an exact address object might be used by another instance of that address), but the end results should still fulfil the enforcing mode invariants and shouldn't compromise security.

### Known limitations

N/A



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6033cd85..d3cfbb76 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -349,6 +349,12 @@ dependencies = [
  "syn 2.0.16",
 ]
 
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -1002,6 +1008,16 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
+[[package]]
+name = "pretty_assertions"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
 [[package]]
 name = "proc-macro-crate"
 version = "1.3.1"
@@ -1370,6 +1386,7 @@ dependencies = [
  "num-derive",
  "num-integer",
  "num-traits",
+ "pretty_assertions",
  "rand",
  "rand_chacha",
  "sha2",
@@ -2060,6 +2077,12 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "yansi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+
 [[package]]
 name = "zeroize"
 version = "1.6.0"
diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml
index aaeaae73..3e52eb01 100644
--- a/soroban-env-host/Cargo.toml
+++ b/soroban-env-host/Cargo.toml
@@ -50,6 +50,7 @@ wasmprinter = "0.2.41"
 expect-test = "1.4.0"
 more-asserts = "0.3.1"
 linregress = "0.5.1"
+pretty_assertions = "1.4.0"
 
 [features]
 testutils = ["soroban-env-common/testutils"]
diff --git a/soroban-env-host/src/auth.rs b/soroban-env-host/src/auth.rs
index af0698a3..ebdc083a 100644
--- a/soroban-env-host/src/auth.rs
+++ b/soroban-env-host/src/auth.rs
@@ -741,7 +741,8 @@ impl AuthorizationManager {
             // ->address.require_auth(). Thus we simply skip the trackers that
             // have already been borrowed.
             if let Ok(mut tracker) = tracker.try_borrow_mut() {
-                // If address doesn't match, just skip the tracker.
+                // If tracker has already been used for this frame or the address
+                // doesn't match, just skip the tracker.
                 if !host.compare(&tracker.address, &address)?.is_eq() {
                     continue;
                 }
@@ -788,6 +789,11 @@ impl AuthorizationManager {
             AuthorizationMode::Enforcing => self.require_auth_enforcing(host, address, &function),
             // metering: free for recording
             AuthorizationMode::Recording(recording_info) => {
+                // At first, try to find the tracker for this exact address
+                // object.
+                // This is a best-effort heuristic to come up with a reasonably
+                // looking recording tree for cases when multiple instances of
+                // the same exact address are used.
                 let address_obj_handle = address.get_handle();
                 let existing_tracker_id = recording_info
                     .try_borrow_tracker_by_address_handle(host)?
@@ -821,6 +827,35 @@ impl AuthorizationManager {
                         ));
                     }
                 }
+                // If there is no active tracker for this exact address object,
+                // try to find any matching active tracker for the address.
+                for tracker in self.try_borrow_account_trackers(host)?.iter() {
+                    if let Ok(mut tracker) = tracker.try_borrow_mut() {
+                        if !host.compare(&tracker.address, &address)?.is_eq() {
+                            continue;
+                        }
+                        // Take the first tracker that is still active (i.e. has
+                        // active authorizations in the current call stack) and
+                        // hasn't been used for this stack frame yet.
+                        if tracker.has_authorized_invocations_in_stack()
+                            && !tracker.current_frame_is_already_matched()
+                        {
+                            return tracker.record_invocation(host, function);
+                        }
+                    } else {
+                        return Err(host.err(
+                            ScErrorType::Auth,
+                            ScErrorCode::InternalError,
+                            "unexpected borrowed tracker in recording auth mode",
+                            &[],
+                        ));
+                    }
+                }
+                // At this stage there is no active tracker to which we could
+                // match the current invocation, thus we need to create a new
+                // tracker.
+                // Alert the user in `disable_non_root_auth` mode if we're not
+                // in the root stack frame.
                 if recording_info.disable_non_root_auth
                     && self.try_borrow_call_stack(host)?.len() != 1
                 {
@@ -1297,11 +1332,7 @@ impl InvocationTracker {
         host: &Host,
         function: AuthorizedFunction,
     ) -> Result<(), HostError> {
-        let frame_is_already_authorized = match self.invocation_id_in_call_stack.last() {
-            Some(Some(_)) => true,
-            _ => false,
-        };
-        if frame_is_already_authorized {
+        if self.current_frame_is_already_matched() {
             return Err(host.err(
                 ScErrorType::Auth,
                 ScErrorCode::ExistingValue,


Test patch:
diff --git a/soroban-env-host/src/test/auth.rs b/soroban-env-host/src/test/auth.rs
index 6dba8925..cfe6f23e 100644
--- a/soroban-env-host/src/test/auth.rs
+++ b/soroban-env-host/src/test/auth.rs
@@ -20,6 +20,8 @@ use soroban_env_common::{AddressObject, Env, Symbol, SymbolStr, TryFromVal, TryI
 
 use crate::native_contract::base_types::Vec as HostVec;
 
+use pretty_assertions::assert_eq;
+
 #[derive(Clone)]
 #[contracttype]
 pub struct ContractTreeNode {
@@ -81,6 +83,15 @@ impl SignNode {
             args: VecM::default(),
         }
     }
+
+    fn tree_fn_stored(contract_id: &Address, children: Vec<SignNode>) -> Self {
+        Self {
+            contract_address: contract_id.clone(),
+            children,
+            fn_name: Symbol::try_from_small_str("stree_fn").unwrap(),
+            args: VecM::default(),
+        }
+    }
 }
 
 // This test uses `AuthContract::tree_fn` to build various authorization trees.
@@ -139,10 +150,40 @@ impl AuthTest {
         root: &SetupNode,
         sign_payloads: Vec<Vec<SignNode>>,
         success: bool,
+    ) {
+        self.tree_test_enforcing_with_addresses(self.get_addresses(), root, sign_payloads, success);
+    }
+
+    fn tree_test_enforcing_with_addresses(
+        &mut self,
+        addresses: HostVec,
+        root: &SetupNode,
+        sign_payloads: Vec<Vec<SignNode>>,
+        success: bool,
     ) {
         self.test_enforcing(
             root.contract_address.clone(),
             Symbol::try_from_small_str("tree_fn").unwrap(),
+            host_vec![&self.host, addresses, self.convert_setup_tree(&root)],
+            sign_payloads,
+            success,
+        );
+    }
+
+    // Like tree_test_enforcing, but read the set of the addreses from storage
+    // for the first authorization.
+    // Covers scenarios when `require_auth` is called for the addresses computed
+    // programmatically (as opposed to passing them as inputs);
+    fn tree_test_enforcing_with_stored_addresses(
+        &mut self,
+        root: &SetupNode,
+        sign_payloads: Vec<Vec<SignNode>>,
+        success: bool,
+    ) {
+        self.store_addresses(root);
+        self.test_enforcing(
+            root.contract_address.clone(),
+            Symbol::try_from_small_str("stree_fn").unwrap(),
             host_vec![
                 &self.host,
                 self.get_addresses(),
@@ -153,6 +194,16 @@ impl AuthTest {
         );
     }
 
+    fn store_addresses(&self, root: &SetupNode) {
+        self.host
+            .call(
+                root.contract_address.clone().into(),
+                Symbol::try_from_small_str("store").unwrap(),
+                host_vec![&self.host, self.get_addresses()].into(),
+            )
+            .unwrap();
+    }
+
     fn test_enforcing(
         &mut self,
         contract_address: Address,
@@ -238,13 +289,49 @@ impl AuthTest {
 
     // Runs `tree_fn` corresponding to the provided setup in recordind mode and
     // returns the recorded payloads.
-    fn tree_run_recording(&self, root: &SetupNode) -> Vec<RecordedAuthPayload> {
+    fn tree_run_recording(
+        &self,
+        root: &SetupNode,
+        disable_non_root_auth: bool,
+    ) -> Vec<RecordedAuthPayload> {
+        let addresses = self.get_addresses();
+        let tree = self.convert_setup_tree(&root);
+        self.run_recording(
+            &root.contract_address,
+            Symbol::try_from_small_str("tree_fn").unwrap(),
+            host_vec![&self.host, addresses, tree],
+            disable_non_root_auth,
+        )
+    }
+
+    fn tree_run_recording_with_stored_addresses(
+        &self,
+        root: &SetupNode,
+        disable_non_root_auth: bool,
+    ) -> Vec<RecordedAuthPayload> {
+        self.store_addresses(root);
         let addresses = self.get_addresses();
+        let tree = self.convert_setup_tree(&root);
+        self.run_recording(
+            &root.contract_address,
+            Symbol::try_from_small_str("stree_fn").unwrap(),
+            host_vec![&self.host, addresses, tree],
+            disable_non_root_auth,
+        )
+    }
+
+    fn tree_run_recording_with_custom_addresses(
+        &self,
+        addresses: HostVec,
+        root: &SetupNode,
+        disable_non_root_auth: bool,
+    ) -> Vec<RecordedAuthPayload> {
         let tree = self.convert_setup_tree(&root);
         self.run_recording(
             &root.contract_address,
             Symbol::try_from_small_str("tree_fn").unwrap(),
             host_vec![&self.host, addresses, tree],
+            disable_non_root_auth,
         )
     }
 
@@ -253,8 +340,11 @@ impl AuthTest {
         contract_address: &Address,
         fn_name: Symbol,
         args: HostVec,
+        disable_non_root_auth: bool,
     ) -> Vec<RecordedAuthPayload> {
-        self.host.switch_to_recording_auth(false).unwrap();
+        self.host
+            .switch_to_recording_auth(disable_non_root_auth)
+            .unwrap();
         self.host
             .call(contract_address.clone().into(), fn_name, args.into())
             .unwrap();
@@ -343,7 +433,7 @@ fn test_single_authorized_call() {
     let setup = SetupNode::new(&test.contracts[0], vec![true], vec![]);
     let expected_sign_payloads = vec![vec![SignNode::tree_fn(&test.contracts[0], vec![])]];
     assert_eq!(
-        test.tree_run_recording(&setup),
+        test.tree_run_recording(&setup, true),
         vec![RecordedAuthPayload {
             address: Some(test.key_to_sc_address(&test.keys[0])),
             nonce: Some(0),
@@ -356,7 +446,7 @@ fn test_single_authorized_call() {
     test.verify_nonces_consumed(vec![1]);
 
     assert_eq!(
-        test.tree_run_recording(&setup),
+        test.tree_run_recording(&setup, true),
         vec![RecordedAuthPayload {
             address: Some(test.key_to_sc_address(&test.keys[0])),
             nonce: Some(0),
@@ -418,7 +508,7 @@ fn test_single_authorized_call_for_multiple_addresses() {
         vec![SignNode::tree_fn(&test.contracts[0], vec![])],
     ];
     assert_eq!(
-        test.tree_run_recording(&setup),
+        test.tree_run_recording(&setup, true),
         vec![
             RecordedAuthPayload {
                 address: Some(test.key_to_sc_address(&test.keys[0])),
@@ -473,7 +563,7 @@ fn test_single_authorization_for_one_address_among_multiple() {
     let setup = SetupNode::new(&test.contracts[0], vec![false, true], vec![]);
     let expected_sign_payloads = vec![vec![], vec![SignNode::tree_fn(&test.contracts[0], vec![])]];
     assert_eq!(
-        test.tree_run_recording(&setup),
+        test.tree_run_recording(&setup, true),
         vec![RecordedAuthPayload {
             address: Some(test.key_to_sc_address(&test.keys[1])),
             nonce: Some(0),
@@ -541,7 +631,7 @@ fn test_single_authorized_call_tree() {
         ],
     );
     assert_eq!(
-        test.tree_run_recording(&setup),
+        test.tree_run_recording(&setup, true),
         vec![RecordedAuthPayload {
             address: Some(test.key_to_sc_address(&test.keys[0])),
             nonce: Some(0),
@@ -639,7 +729,7 @@ fn test_single_authorized_call_tree() {
                     &test.contracts[2],
                     vec![
                         SignNode::tree_fn(&test.contracts[3], vec![]),
-                        SignNode::tree_fn(&test.contracts[4], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
                     ],
                 ),
             ],
@@ -656,8 +746,6 @@ fn test_single_authorized_call_tree() {
                 SignNode::tree_fn(
                     &test.contracts[4], // Should have been 1
                     vec![
-                        // Invocation order shouldn't matter here and purposely
-                        // doesn't match the call tree order
                         SignNode::tree_fn(&test.contracts[2], vec![]),
                         SignNode::tree_fn(&test.contracts[3], vec![]),
                     ],
@@ -686,8 +774,6 @@ fn test_single_authorized_call_tree() {
                     SignNode::tree_fn(
                         &test.contracts[1],
                         vec![
-                            // Invocation order shouldn't matter here and purposely
-                            // doesn't match the call tree order
                             SignNode::tree_fn(&test.contracts[2], vec![]),
                             SignNode::tree_fn(&test.contracts[3], vec![]),
                         ],
@@ -763,7 +849,8 @@ fn test_two_authorized_trees() {
     );
 
     assert_eq!(
-        test.tree_run_recording(&setup),
+        // Root call is not authorized, disable non-root auth check.
+        test.tree_run_recording(&setup, false),
         vec![
             RecordedAuthPayload {
                 address: Some(test.key_to_sc_address(&test.keys[0])),
@@ -906,7 +993,8 @@ fn test_three_authorized_trees() {
     );
 
     assert_eq!(
-        test.tree_run_recording(&setup),
+        // Root call is not authorized, disable non-root auth check.
+        test.tree_run_recording(&setup, false),
         vec![
             RecordedAuthPayload {
                 address: Some(test.key_to_sc_address(&test.keys[0])),
@@ -1048,7 +1136,8 @@ fn test_multi_address_trees() {
     );
 
     assert_eq!(
-        test.tree_run_recording(&setup),
+        // One of the root calls is not authorized, disable non-root auth check.
+        test.tree_run_recording(&setup, false),
         vec![
             RecordedAuthPayload {
                 address: Some(test.key_to_sc_address(&test.keys[0])),
@@ -1204,6 +1293,9 @@ fn test_out_of_order_auth() {
                 test.key_to_address(&test.keys[0]),
                 test.contracts[1]
             ],
+            // Root auth happens after the sub-contract call, so don't disable
+            // non-root auth.
+            false
         ),
         vec![
             RecordedAuthPayload {
@@ -1350,7 +1442,7 @@ fn test_invoker_subcontract_auth() {
     let args = host_vec![&test.host, tree];
     let fn_name = Symbol::try_from_val(&test.host, &"invoker_auth_fn").unwrap();
     assert_eq!(
-        test.run_recording(&test.contracts[5], fn_name, args.clone(),),
+        test.run_recording(&test.contracts[5], fn_name, args.clone(), true),
         vec![]
     );
 
@@ -1438,7 +1530,7 @@ fn test_invoker_subcontract_auth_without_subcontract_calls() {
     let args = host_vec![&test.host, tree];
     let fn_name = Symbol::try_from_val(&test.host, &"invoker_auth_fn_no_call").unwrap();
     assert_eq!(
-        test.run_recording(&test.contracts[5], fn_name, args.clone(),),
+        test.run_recording(&test.contracts[5], fn_name, args.clone(), true),
         vec![]
     );
 
@@ -1718,3 +1810,865 @@ fn test_require_auth_for_self_within_check_auth() {
     assert!(err.error.is_type(ScErrorType::Auth));
     assert!(err.error.is_code(ScErrorCode::InvalidAction));
 }
+
+#[test]
+fn test_multi_address_auth_with_stored_addresses() {
+    let mut test = AuthTest::setup(3, 5);
+    let setup = SetupNode::new(
+        &test.contracts[0],
+        vec![true, true, true],
+        vec![
+            SetupNode::new(
+                &test.contracts[1],
+                vec![true, false, false],
+                vec![
+                    SetupNode::new(&test.contracts[3], vec![true, true, false], vec![]),
+                    SetupNode::new(&test.contracts[2], vec![true, true, false], vec![]),
+                ],
+            ),
+            SetupNode::new(
+                &test.contracts[2],
+                vec![true, true, false],
+                vec![
+                    SetupNode::new(&test.contracts[4], vec![false, false, false], vec![]),
+                    SetupNode::new(&test.contracts[3], vec![true, true, false], vec![]),
+                ],
+            ),
+            SetupNode::new(
+                &test.contracts[1],
+                vec![true, false, false],
+                vec![
+                    SetupNode::new(&test.contracts[3], vec![true, true, false], vec![]),
+                    SetupNode::new(&test.contracts[2], vec![true, true, false], vec![]),
+                ],
+            ),
+        ],
+    );
+    assert_eq!(
+        test.tree_run_recording_with_stored_addresses(&setup, true),
+        vec![
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[0])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn_stored(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![SignNode::tree_fn(&test.contracts[3], vec![])],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                    ],
+                )),
+            },
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[1])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn_stored(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![SignNode::tree_fn(&test.contracts[3], vec![])],
+                        ),
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                    ],
+                )),
+            },
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[2])),
+                nonce: Some(0),
+                invocation: test
+                    .convert_sign_node(&SignNode::tree_fn_stored(&test.contracts[0], vec![])),
+            },
+        ]
+    );
+
+    // Correct call
+    test.tree_test_enforcing_with_stored_addresses(
+        &setup,
+        vec![
+            vec![SignNode::tree_fn_stored(
+                &test.contracts[0],
+                vec![
+                    SignNode::tree_fn(
+                        &test.contracts[1],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                            SignNode::tree_fn(&test.contracts[2], vec![]),
+                        ],
+                    ),
+                    SignNode::tree_fn(
+                        &test.contracts[2],
+                        vec![SignNode::tree_fn(&test.contracts[3], vec![])],
+                    ),
+                    SignNode::tree_fn(
+                        &test.contracts[1],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                            SignNode::tree_fn(&test.contracts[2], vec![]),
+                        ],
+                    ),
+                ],
+            )],
+            vec![SignNode::tree_fn_stored(
+                &test.contracts[0],
+                vec![
+                    SignNode::tree_fn(&test.contracts[3], vec![]),
+                    SignNode::tree_fn(&test.contracts[2], vec![]),
+                    SignNode::tree_fn(
+                        &test.contracts[2],
+                        vec![SignNode::tree_fn(&test.contracts[3], vec![])],
+                    ),
+                    SignNode::tree_fn(&test.contracts[3], vec![]),
+                    SignNode::tree_fn(&test.contracts[2], vec![]),
+                ],
+            )],
+            vec![SignNode::tree_fn_stored(&test.contracts[0], vec![])],
+        ],
+        true,
+    );
+    test.verify_nonces_consumed(vec![1, 1, 1]);
+
+    // Failing scenarios
+    // Disjoint sub-tree for the second account.
+    test.tree_test_enforcing_with_stored_addresses(
+        &setup,
+        vec![
+            vec![SignNode::tree_fn_stored(
+                &test.contracts[0],
+                vec![
+                    SignNode::tree_fn(
+                        &test.contracts[1],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                            SignNode::tree_fn(&test.contracts[2], vec![]),
+                        ],
+                    ),
+                    SignNode::tree_fn(
+                        &test.contracts[2],
+                        vec![SignNode::tree_fn(&test.contracts[3], vec![])],
+                    ),
+                    SignNode::tree_fn(
+                        &test.contracts[1],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                            SignNode::tree_fn(&test.contracts[2], vec![]),
+                        ],
+                    ),
+                ],
+            )],
+            vec![SignNode::tree_fn_stored(
+                &test.contracts[0],
+                vec![
+                    SignNode::tree_fn(&test.contracts[3], vec![]),
+                    SignNode::tree_fn(&test.contracts[2], vec![]),
+                    SignNode::tree_fn(&test.contracts[2], vec![]),
+                    SignNode::tree_fn(&test.contracts[3], vec![]),
+                    SignNode::tree_fn(&test.contracts[3], vec![]),
+                    SignNode::tree_fn(&test.contracts[2], vec![]),
+                ],
+            )],
+            vec![SignNode::tree_fn_stored(&test.contracts[0], vec![])],
+        ],
+        false,
+    );
+
+    // Disjoint top-level tree for the first account
+    test.tree_test_enforcing_with_stored_addresses(
+        &setup,
+        vec![
+            vec![
+                SignNode::tree_fn_stored(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![SignNode::tree_fn(&test.contracts[3], vec![])],
+                        ),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[1],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                    ],
+                ),
+            ],
+            vec![SignNode::tree_fn_stored(
+                &test.contracts[0],
+                vec![
+                    SignNode::tree_fn(&test.contracts[3], vec![]),
+                    SignNode::tree_fn(&test.contracts[2], vec![]),
+                    SignNode::tree_fn(
+                        &test.contracts[2],
+                        vec![SignNode::tree_fn(&test.contracts[3], vec![])],
+                    ),
+                    SignNode::tree_fn(&test.contracts[3], vec![]),
+                    SignNode::tree_fn(&test.contracts[2], vec![]),
+                ],
+            )],
+            vec![SignNode::tree_fn_stored(&test.contracts[0], vec![])],
+        ],
+        false,
+    );
+}
+
+#[test]
+fn test_same_auth_tree_with_duplicate_addresses() {
+    let mut test = AuthTest::setup(2, 5);
+    let setup = SetupNode::new(
+        &test.contracts[0],
+        vec![true, true, true, true],
+        vec![
+            SetupNode::new(
+                &test.contracts[1],
+                vec![true, true, true, true],
+                vec![
+                    SetupNode::new(&test.contracts[3], vec![true, true, true, true], vec![]),
+                    SetupNode::new(&test.contracts[2], vec![true, true, true, true], vec![]),
+                ],
+            ),
+            SetupNode::new(
+                &test.contracts[2],
+                vec![true, true, true, true],
+                vec![
+                    SetupNode::new(&test.contracts[4], vec![true, true, true, true], vec![]),
+                    SetupNode::new(&test.contracts[3], vec![true, true, true, true], vec![]),
+                ],
+            ),
+        ],
+    );
+
+    let addresses = host_vec![
+        &test.host,
+        test.key_to_address(&test.keys[0]).to_val(),
+        test.key_to_address(&test.keys[1]).to_val(),
+        test.key_to_address(&test.keys[1]).to_val(),
+        test.key_to_address(&test.keys[0]).to_val(),
+    ];
+    assert_eq!(
+        test.tree_run_recording_with_custom_addresses(addresses.clone(), &setup, true),
+        vec![
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[0])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                )),
+            },
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[1])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                )),
+            },
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[1])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                )),
+            },
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[0])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                )),
+            },
+        ]
+    );
+
+    // Correct call
+    test.tree_test_enforcing_with_addresses(
+        addresses.clone(),
+        &setup,
+        vec![
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+            ],
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+            ],
+        ],
+        true,
+    );
+    test.verify_nonces_consumed(vec![2, 2]);
+
+    // Failing scenarios
+    // Just one auth per address
+    test.tree_test_enforcing_with_addresses(
+        addresses.clone(),
+        &setup,
+        vec![
+            vec![SignNode::tree_fn(
+                &test.contracts[0],
+                vec![
+                    SignNode::tree_fn(
+                        &test.contracts[1],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                            SignNode::tree_fn(&test.contracts[2], vec![]),
+                        ],
+                    ),
+                    SignNode::tree_fn(
+                        &test.contracts[2],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[4], vec![]),
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                        ],
+                    ),
+                ],
+            )],
+            vec![SignNode::tree_fn(
+                &test.contracts[0],
+                vec![
+                    SignNode::tree_fn(
+                        &test.contracts[1],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                            SignNode::tree_fn(&test.contracts[2], vec![]),
+                        ],
+                    ),
+                    SignNode::tree_fn(
+                        &test.contracts[2],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[4], vec![]),
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                        ],
+                    ),
+                ],
+            )],
+        ],
+        false,
+    );
+
+    // Incorrectly merged trees - it's not allowed to use the same auth tree
+    // multiple times per frame
+    test.tree_test_enforcing_with_addresses(
+        addresses.clone(),
+        &setup,
+        vec![
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                        // Sub-tree calls from the second entry - while
+                        // technically the address has authorized the same call
+                        // tree twice, we don't allow matching it twice in the
+                        // same frame.
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+                SignNode::tree_fn(&test.contracts[0], vec![]),
+            ],
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+            ],
+        ],
+        false,
+    );
+}
+
+#[test]
+fn test_different_auth_trees_with_duplicate_addresses() {
+    let mut test = AuthTest::setup(2, 5);
+    let setup = SetupNode::new(
+        &test.contracts[0],
+        vec![true, true, true, true],
+        vec![
+            SetupNode::new(
+                &test.contracts[1],
+                vec![true, false, true, false],
+                vec![
+                    SetupNode::new(&test.contracts[3], vec![false, true, true, true], vec![]),
+                    SetupNode::new(&test.contracts[2], vec![true, true, true, false], vec![]),
+                ],
+            ),
+            SetupNode::new(
+                &test.contracts[2],
+                vec![false, true, true, false],
+                vec![
+                    SetupNode::new(&test.contracts[4], vec![true, false, true, false], vec![]),
+                    SetupNode::new(&test.contracts[3], vec![false, false, true, true], vec![]),
+                ],
+            ),
+        ],
+    );
+
+    let addresses = host_vec![
+        &test.host,
+        test.key_to_address(&test.keys[0]).to_val(),
+        test.key_to_address(&test.keys[1]).to_val(),
+        test.key_to_address(&test.keys[1]).to_val(),
+        test.key_to_address(&test.keys[0]).to_val(),
+    ];
+    assert_eq!(
+        test.tree_run_recording_with_custom_addresses(addresses.clone(), &setup, true),
+        vec![
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[0])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![SignNode::tree_fn(&test.contracts[2], vec![])],
+                        ),
+                        SignNode::tree_fn(&test.contracts[4], vec![]),
+                    ],
+                )),
+            },
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[1])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                    ],
+                )),
+            },
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[1])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                    ],
+                )),
+            },
+            RecordedAuthPayload {
+                address: Some(test.key_to_sc_address(&test.keys[0])),
+                nonce: Some(0),
+                invocation: test.convert_sign_node(&SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                    ],
+                )),
+            },
+        ]
+    );
+
+    // Correct call
+    test.tree_test_enforcing_with_addresses(
+        addresses.clone(),
+        &setup,
+        vec![
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![SignNode::tree_fn(&test.contracts[2], vec![])],
+                        ),
+                        SignNode::tree_fn(&test.contracts[4], vec![]),
+                    ],
+                ),
+            ],
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                    ],
+                ),
+            ],
+        ],
+        true,
+    );
+    test.verify_nonces_consumed(vec![2, 2]);
+
+    // Failing scenarios
+    // Incorrectly merged trees
+    test.tree_test_enforcing_with_addresses(
+        addresses.clone(),
+        &setup,
+        vec![
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        // We need to authorize both 0->[1 skipped]->3 and
+                        // 0->1->3 call stacks, where 1 is the same contract. As
+                        // it can't be authorized and not authorized at the same
+                        // time, this can't succeed.
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![SignNode::tree_fn(&test.contracts[2], vec![])],
+                        ),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![SignNode::tree_fn(&test.contracts[4], vec![])],
+                ),
+            ],
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[2], vec![]),
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                            ],
+                        ),
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                    ],
+                ),
+            ],
+        ],
+        false,
+    );
+
+    // Another incorrect tree mix
+    test.tree_test_enforcing_with_addresses(
+        addresses.clone(),
+        &setup,
+        vec![
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                    ],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(
+                            &test.contracts[1],
+                            vec![SignNode::tree_fn(&test.contracts[2], vec![])],
+                        ),
+                        SignNode::tree_fn(&test.contracts[4], vec![]),
+                    ],
+                ),
+            ],
+            vec![
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![SignNode::tree_fn(
+                        &test.contracts[1],
+                        vec![
+                            SignNode::tree_fn(&test.contracts[2], vec![]),
+                            SignNode::tree_fn(&test.contracts[3], vec![]),
+                        ],
+                    )],
+                ),
+                SignNode::tree_fn(
+                    &test.contracts[0],
+                    vec![
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                        SignNode::tree_fn(&test.contracts[3], vec![]),
+                        SignNode::tree_fn(&test.contracts[2], vec![]),
+                        // That's the third auth for the contract 2 and we only
+                        // call 2 just twice.
+                        SignNode::tree_fn(
+                            &test.contracts[2],
+                            vec![
+                                SignNode::tree_fn(&test.contracts[3], vec![]),
+                                SignNode::tree_fn(&test.contracts[4], vec![]),
+                            ],
+                        ),
+                    ],
+                ),
+            ],
+        ],
+        false,
+    );
+}
diff --git a/soroban-test-wasms/wasm-workspace/auth/src/lib.rs b/soroban-test-wasms/wasm-workspace/auth/src/lib.rs
index 2feebcfd..86e87595 100644
--- a/soroban-test-wasms/wasm-workspace/auth/src/lib.rs
+++ b/soroban-test-wasms/wasm-workspace/auth/src/lib.rs
@@ -1,7 +1,7 @@
 #![no_std]
 use soroban_sdk::{
     auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation},
-    contract, contractimpl, contracttype, vec, Address, Env, IntoVal, symbol_short, Vec,
+    contract, contractimpl, contracttype, symbol_short, vec, Address, Env, IntoVal, Vec,
 };
 
 #[contract]
@@ -76,6 +76,34 @@ impl AuthContract {
         }
         env.authorize_as_current_contract(auth_entries);
     }
+
+    pub fn store(env: Env, addresses: Vec<Address>) {
+        env.storage()
+            .persistent()
+            .set(&symbol_short!("addr"), &addresses);
+    }
+
+    pub fn stree_fn(env: Env, addresses: Vec<Address>, tree: TreeNode) {
+        let stored_addresses: Vec<Address> = env
+            .storage()
+            .persistent()
+            .get(&symbol_short!("addr"))
+            .unwrap();
+        for i in 0..addresses.len() {
+            if tree.need_auth.get_unchecked(i) {
+                stored_addresses
+                    .get_unchecked(i)
+                    .require_auth_for_args(vec![&env]);
+            }
+        }
+        for child in tree.children.iter() {
+            env.invoke_contract::<()>(
+                &child.contract.clone(),
+                &symbol_short!("tree_fn"),
+                (addresses.clone(), child).into_val(&env),
+            );
+        }
+    }
 }
 
 // Converts the whole provided tree to invoker auth entries, ignoring