Repository: stellar/rs-soroban-env
Pull request #1260 (2023-11-29 00:09:06)

Fix the invoker contract auth rollback logic and cover it in the tests.
### What

Fix the invoker contract auth rollback logic and cover it in the tests.

Besides the 'obvious' issue of missing the code that actually performs rollback, there was much more subtle issue of incorrect ordering between `rollback` and `pop_frame`. To avoid this I've moved the snapshots/rollbacks to the auth manager, so that it's not possible to accidentally mess up that order.

Resolves https://github.com/stellar/rs-soroban-env/issues/1252

### Why

Bug fix/adding tests

### Known limitations

N/A



Patch:
diff --git a/soroban-env-host/src/auth.rs b/soroban-env-host/src/auth.rs
index 860a537b..0966d26b 100644
--- a/soroban-env-host/src/auth.rs
+++ b/soroban-env-host/src/auth.rs
@@ -1110,7 +1110,7 @@ impl AuthorizationManager {
 
     // Returns a snapshot of `AuthorizationManager` to use for rollback.
     // metering: covered
-    pub(crate) fn snapshot(&self, host: &Host) -> Result<AuthorizationManagerSnapshot, HostError> {
+    fn snapshot(&self, host: &Host) -> Result<AuthorizationManagerSnapshot, HostError> {
         let _span = tracy_span!("snapshot auth");
         let account_trackers_snapshot = match &self.mode {
             AuthorizationMode::Enforcing => {
@@ -1143,11 +1143,7 @@ impl AuthorizationManager {
         let invoker_contract_tracker_root_snapshots = self
             .try_borrow_invoker_contract_trackers(host)?
             .iter()
-            .map(|t| {
-                t.invocation_tracker
-                    .root_authorized_invocation
-                    .snapshot(host.as_budget())
-            })
+            .map(|t| t.invocation_tracker.snapshot(host.as_budget()))
             .metered_collect::<Result<Vec<AuthorizedInvocationSnapshot>, HostError>>(host)??;
         #[cfg(any(test, feature = "recording_auth"))]
         let tracker_by_address_handle = match &self.mode {
@@ -1169,7 +1165,7 @@ impl AuthorizationManager {
 
     // Rolls back this `AuthorizationManager` to the snapshot state.
     // metering: covered
-    pub(crate) fn rollback(
+    fn rollback(
         &self,
         host: &Host,
         snapshot: AuthorizationManagerSnapshot,
@@ -1202,7 +1198,7 @@ impl AuthorizationManager {
                                 host.err(
                                     ScErrorType::Auth,
                                     ScErrorCode::InternalError,
-                                    "unexpected bad auth snapshot",
+                                    "unexpected bad auth borrow",
                                     &[],
                                 )
                             })?
@@ -1216,7 +1212,7 @@ impl AuthorizationManager {
             }
         }
 
-        let invoker_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
+        let mut invoker_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
         if invoker_trackers.len() != snapshot.invoker_contract_tracker_root_snapshots.len() {
             return Err(host.err(
                 ScErrorType::Auth,
@@ -1225,6 +1221,12 @@ impl AuthorizationManager {
                 &[],
             ));
         }
+        for (tracker, snapshot) in invoker_trackers
+            .iter_mut()
+            .zip(snapshot.invoker_contract_tracker_root_snapshots.iter())
+        {
+            tracker.invocation_tracker.rollback(snapshot)?;
+        }
 
         #[cfg(any(test, feature = "recording_auth"))]
         if let Some(tracker_by_address_handle) = snapshot.tracker_by_address_handle {
@@ -1269,10 +1271,15 @@ impl AuthorizationManager {
         self.push_tracker_frame(host)
     }
 
-    // Records a new call stack frame.
+    // Records a new call stack frame and returns a snapshot for rolling
+    // back this stack frame.
     // This should be called for every `Host` `push_frame`.
     // metering: covered
-    pub(crate) fn push_frame(&self, host: &Host, frame: &Frame) -> Result<(), HostError> {
+    pub(crate) fn push_frame(
+        &self,
+        host: &Host,
+        frame: &Frame,
+    ) -> Result<AuthorizationManagerSnapshot, HostError> {
         let _span = tracy_span!("push auth frame");
         let (contract_id, function_name) = match frame {
             Frame::ContractVM { vm, fn_name, .. } => {
@@ -1283,7 +1290,7 @@ impl AuthorizationManager {
             // Use the respective push (like
             // `push_create_contract_host_fn_frame`) functions instead to push
             // the frame with the required info.
-            Frame::HostFunction(_) => return Ok(()),
+            Frame::HostFunction(_) => return self.snapshot(host),
             Frame::StellarAssetContract(id, fn_name, ..) => (id.metered_clone(host)?, *fn_name),
             #[cfg(any(test, feature = "testutils"))]
             Frame::TestContract(tc) => (tc.id.metered_clone(host)?, tc.func),
@@ -1296,14 +1303,27 @@ impl AuthorizationManager {
                 function_name,
             }));
 
-        self.push_tracker_frame(host)
+        self.push_tracker_frame(host)?;
+        self.snapshot(host)
     }
 
-    // Pops a call stack frame.
+    // Pops a call stack frame and maybe rolls back the internal
+    // state according to the provided snapshot.
     // This should be called for every `Host` `pop_frame`.
     // metering: covered
-    pub(crate) fn pop_frame(&self, host: &Host) -> Result<(), HostError> {
+    pub(crate) fn pop_frame(
+        &self,
+        host: &Host,
+        snapshot: Option<AuthorizationManagerSnapshot>,
+    ) -> Result<(), HostError> {
         let _span = tracy_span!("pop auth frame");
+        // Important: rollback has to be performed before popping the frame
+        // from the tracker. This ensures correct work of invoker contract
+        // trackers for which snapshots are dependent on the current
+        // call stack.
+        if let Some(snapshot) = snapshot {
+            self.rollback(host, snapshot)?;
+        }
         {
             let mut call_stack = self.try_borrow_call_stack_mut(host)?;
             // Currently we don't push host function call frames, hence this may be
@@ -1331,6 +1351,16 @@ impl AuthorizationManager {
         // the tracker's frame itself is popped). Thus trackers form a stack
         // where the shorter lifetime trackers are at the top.
         while let Some(last) = invoker_contract_trackers.last() {
+            // *Subtle*: there are two possible scenarios when tracker is considered
+            // to be out of scope:
+            // - When the sub-contract call is finished. For example, contract A creates
+            // a tracker, then calls contract B. When we pop the frame of contract B,
+            // the tracker's stack will be empty and thus considered to be out of scope.
+            // - When the contract that created the tracker is going out of scope without
+            // calling any sub-contracts. For example, contract A creates a tracker and
+            // returns. When we pop the frame of contract A, the tracker's stack will be
+            // empty (because it's created empty), and thus considered to be out of scope.
+            // The invariant above is maintained in both scenarios.
             if last.is_out_of_scope() {
                 invoker_contract_trackers.pop();
             } else {
diff --git a/soroban-env-host/src/host/frame.rs b/soroban-env-host/src/host/frame.rs
index 7483e9ed..c5e27901 100644
--- a/soroban-env-host/src/host/frame.rs
+++ b/soroban-env-host/src/host/frame.rs
@@ -145,8 +145,7 @@ impl Host {
     pub(super) fn push_context(&self, ctx: Context) -> Result<RollbackPoint, HostError> {
         let _span = tracy_span!("push context");
         let auth_manager = self.try_borrow_authorization_manager()?;
-        let auth_snapshot = auth_manager.snapshot(self)?;
-        auth_manager.push_frame(self, &ctx.frame)?;
+        let auth_snapshot = auth_manager.push_frame(self, &ctx.frame)?;
         // Establish the rp first, since this might run out of gas and fail.
         let rp = RollbackPoint {
             storage: self.try_borrow_storage()?.map.metered_clone(self)?,
@@ -167,7 +166,6 @@ impl Host {
         let _span = tracy_span!("pop context");
 
         let ctx = self.try_borrow_context_stack_mut()?.pop();
-        self.try_borrow_authorization_manager()?.pop_frame(self)?;
 
         #[cfg(any(test, feature = "recording_auth"))]
         if self.try_borrow_context_stack()?.is_empty() {
@@ -176,13 +174,14 @@ impl Host {
             self.try_borrow_authorization_manager()?
                 .maybe_emulate_authentication(self)?;
         }
-
+        let mut auth_snapshot = None;
         if let Some(rp) = orp {
             self.try_borrow_storage_mut()?.map = rp.storage;
             self.try_borrow_events_mut()?.rollback(rp.events)?;
-            self.try_borrow_authorization_manager()?
-                .rollback(self, rp.auth)?;
+            auth_snapshot = Some(rp.auth);
         }
+        self.try_borrow_authorization_manager()?
+            .pop_frame(self, auth_snapshot)?;
         ctx.ok_or_else(|| {
             self.err(
                 ScErrorType::Context,
diff --git a/soroban-env-host/src/host/lifecycle.rs b/soroban-env-host/src/host/lifecycle.rs
index 9c66abfd..7259f75e 100644
--- a/soroban-env-host/src/host/lifecycle.rs
+++ b/soroban-env-host/src/host/lifecycle.rs
@@ -94,7 +94,8 @@ impl Host {
         // for them just to make auth work in a single case).
         let res = self.create_contract_with_optional_auth(deployer, args);
         if has_deployer {
-            self.try_borrow_authorization_manager()?.pop_frame(self)?;
+            self.try_borrow_authorization_manager()?
+                .pop_frame(self, None)?;
         }
         res
     }


Test patch:
diff --git a/soroban-env-host/src/test/auth.rs b/soroban-env-host/src/test/auth.rs
index 00de1aad..79442a88 100644
--- a/soroban-env-host/src/test/auth.rs
+++ b/soroban-env-host/src/test/auth.rs
@@ -278,12 +278,16 @@ impl AuthTest {
         }
 
         self.host.set_authorization_entries(contract_auth).unwrap();
-        assert_eq!(
-            self.host
-                .call(contract_address.into(), fn_name, args.into())
-                .is_ok(),
-            success
-        );
+        let res = self
+            .host
+            .call(contract_address.into(), fn_name, args.into());
+        if res.is_err() && success {
+            eprintln!(
+                "invocation failed while success expected, error: {:?}",
+                res.clone().err().unwrap()
+            );
+        }
+        assert_eq!(res.is_ok(), success);
     }
 
     fn read_nonce_live_until(&self, address: &Address, nonce: i64) -> Option<u32> {
@@ -372,6 +376,28 @@ impl AuthTest {
         self.host.get_recorded_auth_payloads().unwrap()
     }
 
+    fn test_recording_and_enforcing_no_auth(
+        &mut self,
+        contract_address: Address,
+        fn_name: Symbol,
+        args: HostVec,
+        success: bool,
+    ) {
+        self.host.switch_to_recording_auth(true).unwrap();
+        assert_eq!(
+            self.host
+                .call(
+                    contract_address.clone().into(),
+                    fn_name,
+                    args.clone().into()
+                )
+                .is_ok(),
+            success
+        );
+
+        self.test_enforcing(contract_address.clone(), fn_name, args, vec![], success);
+    }
+
     fn key_to_sc_address(&self, key: &SigningKey) -> ScAddress {
         ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
             key.verifying_key().to_bytes(),
@@ -1647,20 +1673,101 @@ fn test_invoker_subcontract_auth() {
     );
 
     let tree = test.convert_setup_tree(&setup);
-    let args = test_vec![&test.host, tree];
+    let args = test_vec![&test.host, &tree, &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(), true),
-        vec![]
+    test.test_recording_and_enforcing_no_auth(
+        test.contracts[5].clone(),
+        fn_name,
+        args.clone(),
+        true,
     );
+}
 
-    test.test_enforcing(
+#[test]
+fn test_invoker_subcontract_auth_with_duplicates() {
+    let mut test = AuthTest::setup(0, 6);
+
+    let setup = SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![
+            SetupNode::new(
+                &test.contracts[1],
+                vec![true],
+                vec![
+                    SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                    SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                ],
+            ),
+            SetupNode::new(
+                &test.contracts[1],
+                vec![true],
+                vec![
+                    SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                    SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                ],
+            ),
+        ],
+    );
+
+    let tree = test.convert_setup_tree(&setup);
+    let fn_name = Symbol::try_from_val(&test.host, &"invoker_auth_fn").unwrap();
+    test.test_recording_and_enforcing_no_auth(
         test.contracts[5].clone(),
         fn_name,
-        args.clone(),
-        vec![],
+        test_vec![&test.host, &tree, &tree],
         true,
-    )
+    );
+    // Remove the top-level duplicate node from contract's authorizations - the call
+    // must fail now.
+    let incomplete_tree = test.convert_setup_tree(&SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![SetupNode::new(
+            &test.contracts[1],
+            vec![true],
+            vec![
+                SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                SetupNode::new(&test.contracts[2], vec![true], vec![]),
+            ],
+        )],
+    ));
+    test.test_recording_and_enforcing_no_auth(
+        test.contracts[5].clone(),
+        fn_name,
+        test_vec![&test.host, &tree, &incomplete_tree],
+        false,
+    );
+    // Remove the inner duplicate node from contract's authorizations - the call
+    // must fail now.
+    let incomplete_tree = test.convert_setup_tree(&SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![
+            SetupNode::new(
+                &test.contracts[1],
+                vec![true],
+                vec![
+                    SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                    SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                ],
+            ),
+            SetupNode::new(
+                &test.contracts[1],
+                vec![true],
+                vec![
+                    SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                    // Missing one auth here.
+                ],
+            ),
+        ],
+    ));
+    test.test_recording_and_enforcing_no_auth(
+        test.contracts[5].clone(),
+        fn_name,
+        test_vec![&test.host, &tree, &incomplete_tree],
+        false,
+    );
 }
 
 #[test]
@@ -1676,11 +1783,11 @@ fn test_invoker_subcontract_with_gaps() {
     );
 
     let fn_name = Symbol::try_from_val(&test.host, &"invoker_auth_fn").unwrap();
-
+    let top_gap_tree = test.convert_setup_tree(&top_gap_setup);
     test.test_enforcing(
         test.contracts[3].clone(),
         fn_name,
-        test_vec![&test.host, test.convert_setup_tree(&top_gap_setup)],
+        test_vec![&test.host, &top_gap_tree, &top_gap_tree],
         vec![],
         true,
     );
@@ -1696,16 +1803,161 @@ fn test_invoker_subcontract_with_gaps() {
             vec![SetupNode::new(&test.contracts[2], vec![true], vec![])],
         )],
     );
-
+    let mid_gap_tree = test.convert_setup_tree(&mid_gap_setup);
     test.test_enforcing(
         test.contracts[3].clone(),
         fn_name,
-        test_vec![&test.host, test.convert_setup_tree(&mid_gap_setup)],
+        test_vec![&test.host, &mid_gap_tree, &mid_gap_tree],
         vec![],
         false,
     );
 }
 
+#[test]
+fn test_invoker_subcontract_auth_with_rollbacks() {
+    let mut test = AuthTest::setup(0, 7);
+
+    // Setup the call tree that allows contract failures at different levels.
+    let call_setup = SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![
+            // Allow auth failure right after the root call (root call itself is authorized by
+            // default).
+            SetupNode::new_try_call(&test.contracts[1], vec![true], vec![]),
+            SetupNode::new(&test.contracts[2], vec![true], vec![]),
+            // Add a nested call that might fail in sub-calls.
+            SetupNode::new_try_call(
+                &test.contracts[3],
+                vec![true],
+                vec![SetupNode::new(&test.contracts[4], vec![true], vec![])],
+            ),
+            // Add a nested call that is expected to succeed.
+            SetupNode::new(
+                &test.contracts[3],
+                vec![true],
+                vec![
+                    // Allow failure deeper in the stack.
+                    SetupNode::new_try_call(&test.contracts[4], vec![true], vec![]),
+                    SetupNode::new(&test.contracts[5], vec![true], vec![]),
+                ],
+            ),
+        ],
+    );
+
+    let call_tree = test.convert_setup_tree(&call_setup);
+    let fn_name = Symbol::try_from_val(&test.host, &"invoker_auth_fn").unwrap();
+
+    // Authorize the whole tree - should succeed.
+    test.test_recording_and_enforcing_no_auth(
+        test.contracts[6].clone(),
+        fn_name,
+        test_vec![&test.host, &call_tree, &call_tree],
+        true,
+    );
+    // Skip authorizing the fallible calls - this should still succeed.
+    let tree_without_optional_auth = test.convert_setup_tree(&SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![
+            SetupNode::new(&test.contracts[2], vec![true], vec![]),
+            // Note, that this should be matched to try_call for contract 3 first, then
+            // rolled back due to failure, then used again for 3->5 auth.
+            SetupNode::new(
+                &test.contracts[3],
+                vec![true],
+                vec![SetupNode::new(&test.contracts[5], vec![true], vec![])],
+            ),
+        ],
+    ));
+    test.test_recording_and_enforcing_no_auth(
+        test.contracts[6].clone(),
+        fn_name,
+        test_vec![&test.host, &call_tree, &tree_without_optional_auth],
+        true,
+    );
+    // Authorize only fallible calls, but not infallible - now invocation should fail.
+    let tree_without_mandatory_auth = test.convert_setup_tree(&SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![
+            // Allow auth failure right after the root call (root call itself is authorized by
+            // default).
+            SetupNode::new(&test.contracts[1], vec![true], vec![]),
+            // Add a nested call that might fail in sub-calls.
+            SetupNode::new(
+                &test.contracts[3],
+                vec![true],
+                vec![SetupNode::new(&test.contracts[4], vec![true], vec![])],
+            ),
+        ],
+    ));
+    test.test_recording_and_enforcing_no_auth(
+        test.contracts[6].clone(),
+        fn_name,
+        test_vec![&test.host, &call_tree, &tree_without_mandatory_auth],
+        false,
+    );
+    // Add a mix of fallible and mandatory auth, with mandatory auth missing.
+    let tree_with_incorrect_auth = test.convert_setup_tree(&SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![
+            // Allow auth failure right after the root call (root call itself is authorized by
+            // default).
+            SetupNode::new(&test.contracts[1], vec![true], vec![]),
+            SetupNode::new(&test.contracts[2], vec![true], vec![]),
+            // Add a nested call that is expected to succeed.
+            SetupNode::new(
+                &test.contracts[3],
+                vec![true],
+                vec![
+                    // Allow failure deeper in the stack.
+                    SetupNode::new_try_call(&test.contracts[4], vec![true], vec![]),
+                    // Incorrect auth - this needs to be contract 5 to pass.
+                    SetupNode::new(&test.contracts[2], vec![true], vec![]),
+                ],
+            ),
+        ],
+    ));
+    test.test_recording_and_enforcing_no_auth(
+        test.contracts[6].clone(),
+        fn_name,
+        test_vec![&test.host, &call_tree, &tree_with_incorrect_auth],
+        false,
+    );
+}
+
+#[test]
+fn test_invoker_subcontract_auth_is_not_reused() {
+    let mut test = AuthTest::setup(0, 4);
+
+    let call_tree = SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![SetupNode::new(&test.contracts[1], vec![true], vec![])],
+    );
+    let failing_tree = SetupNode::new(
+        &test.contracts[0],
+        vec![true],
+        vec![SetupNode::new(&test.contracts[2], vec![true], vec![])],
+    );
+
+    let args = test_vec![
+        &test.host,
+        test.convert_setup_tree(&call_tree),
+        test.convert_setup_tree(&failing_tree)
+    ];
+    let fn_name = Symbol::try_from_val(&test.host, &"invoker_auth_no_reuse").unwrap();
+
+    test.test_recording_and_enforcing_no_auth(
+        test.contracts[3].clone(),
+        fn_name,
+        args.clone(),
+        true,
+    );
+}
+
 #[test]
 fn test_invoker_subcontract_auth_without_subcontract_calls() {
     let mut test = AuthTest::setup(0, 6);
@@ -1737,16 +1989,10 @@ fn test_invoker_subcontract_auth_without_subcontract_calls() {
     let tree = test.convert_setup_tree(&setup);
     let args = test_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(), true),
-        vec![]
-    );
-
-    test.test_enforcing(
+    test.test_recording_and_enforcing_no_auth(
         test.contracts[5].clone(),
         fn_name,
         args.clone(),
-        vec![],
         true,
     );
 }
diff --git a/soroban-test-wasms/wasm-workspace/auth/src/lib.rs b/soroban-test-wasms/wasm-workspace/auth/src/lib.rs
index 95e70a65..5476ac7a 100644
--- a/soroban-test-wasms/wasm-workspace/auth/src/lib.rs
+++ b/soroban-test-wasms/wasm-workspace/auth/src/lib.rs
@@ -1,8 +1,8 @@
 #![no_std]
 
 use soroban_sdk::{
-    auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}, Error,
-    contract, contractimpl, contracttype, symbol_short, vec, Address, Env, IntoVal, Vec,
+    auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation},
+    contract, contractimpl, contracttype, symbol_short, vec, Address, Env, Error, IntoVal, Vec,
 };
 
 #[contract]
@@ -60,11 +60,11 @@ impl AuthContract {
         addr.require_auth();
     }
 
-    pub fn invoker_auth_fn(env: Env, tree: TreeNode) {
-        // Build auth entries from children - tree root auth works
+    pub fn invoker_auth_fn(env: Env, tree: TreeNode, auth_tree: TreeNode) {
+        // Build auth entries from `auth_tree` children - tree root auth works
         // automatically.
         let mut auth_entries = vec![&env];
-        for child in tree.children.iter() {
+        for child in auth_tree.children.iter() {
             auth_entries.push_back(tree_to_invoker_contract_auth(&env, &child));
         }
         env.authorize_as_current_contract(auth_entries);
@@ -87,6 +87,58 @@ impl AuthContract {
         env.authorize_as_current_contract(auth_entries);
     }
 
+    // Function for specifically testing that invoker contract auth is never reused,
+    // whether the contract call fails or not.
+    // `failing_tree` should be different from `tree` (so that `tree` auth is not
+    // suitable for it).
+    // Note: the contract could build both trees itself, we only pass them externally
+    // for convenience (there are test helpers for building these trees).
+    pub fn invoker_auth_no_reuse(env: Env, tree: TreeNode, failing_tree: TreeNode) {
+        let curr_address = env.current_contract_address();
+        let mut auth_entries = vec![&env];
+        for child in tree.children.iter() {
+            auth_entries.push_back(tree_to_invoker_contract_auth(&env, &child));
+        }
+        env.authorize_as_current_contract(auth_entries.clone());
+        // Make sure authorization succeeds for the `tree`.
+        assert!(env
+            .try_invoke_contract::<(), Error>(
+                &tree.contract,
+                &symbol_short!("tree_fn"),
+                (vec![&env, curr_address.clone()], tree.clone()).into_val(&env),
+            )
+            .is_ok());
+        // Now the identical call should fail because auth is consumed.
+        assert!(env
+            .try_invoke_contract::<(), Error>(
+                &tree.contract,
+                &symbol_short!("tree_fn"),
+                (vec![&env, curr_address.clone()], tree.clone()).into_val(&env),
+            )
+            .is_err());
+
+        // Authorize the next call again.
+        env.authorize_as_current_contract(auth_entries.clone());
+        let curr_address = env.current_contract_address();
+        // Use the `failing_tree` that requires different auth entries.
+        assert!(env
+            .try_invoke_contract::<(), Error>(
+                &tree.contract,
+                &symbol_short!("tree_fn"),
+                (vec![&env, curr_address.clone()], failing_tree).into_val(&env),
+            )
+            .is_err());
+        // The 'valid' call should still fail because auth entries are
+        // consumed even by the failing invocations.
+        assert!(env
+            .try_invoke_contract::<(), Error>(
+                &tree.contract,
+                &symbol_short!("tree_fn"),
+                (vec![&env, curr_address.clone()], tree.clone()).into_val(&env),
+            )
+            .is_err());
+    }
+
     pub fn store(env: Env, addresses: Vec<Address>) {
         env.storage()
             .persistent()
@@ -133,11 +185,7 @@ impl AuthContract {
             (address.clone(), 123_u32).into_val(&env),
         );
         // Call `allow` on the account contract.
-        env.invoke_contract::<()>(
-            &address,
-            &symbol_short!("allow"),
-            ().into_val(&env),
-        );
+        env.invoke_contract::<()>(&address, &symbol_short!("allow"), ().into_val(&env));
         env.invoke_contract::<()>(
             &contract_address,
             &symbol_short!("do_auth"),