diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py
index 63e31d9061873fc88676efe0dae7cdccb8b95876..01aac767eff4b89129fdb58fb427c3d5e01631b1 100644
--- a/src/_zkapauthorizer/_storage_client.py
+++ b/src/_zkapauthorizer/_storage_client.py
@@ -22,6 +22,7 @@ implemented in ``_storage_server.py``.
 
 from functools import (
     partial,
+    wraps,
 )
 
 import attr
@@ -92,6 +93,15 @@ def call_with_passes(method, num_passes, get_passes):
     """
     def get_more_passes(reason):
         reason.trap(MorePassesRequired)
+        if len(reason.value.signature_check_failed) == 0:
+            # If no signature checks failed then the call just didn't supply
+            # enough passes.  The exception tells us how many passes we should
+            # spend so we could try again with that number of passes but for
+            # now we'll just let the exception propagate.  The client should
+            # always figure out the number of passes right on the first try so
+            # this case is somewhat suspicious.  Err on the side of lack of
+            # service instead of burning extra passes.
+            return reason
         new_passes = get_passes(len(reason.value.signature_check_failed))
         for idx, new_pass in zip(reason.value.signature_check_failed, new_passes):
             passes[idx] = new_pass
@@ -106,6 +116,20 @@ def call_with_passes(method, num_passes, get_passes):
     return go(passes)
 
 
+def with_rref(f):
+    """
+    Decorate a function so that it automatically receives a
+    ``RemoteReference`` as its first argument when called.
+
+    The ``RemoteReference`` is retrieved by calling ``_rref`` on the first
+    argument passed to the function (expected to be ``self``).
+    """
+    @wraps(f)
+    def g(self, *args, **kwargs):
+        return f(self, self._rref(), *args, **kwargs)
+    return g
+
+
 @implementer(IStorageServer)
 @attr.s
 class ZKAPAuthorizerStorageClient(object):
@@ -138,7 +162,6 @@ class ZKAPAuthorizerStorageClient(object):
     _get_rref = attr.ib()
     _get_passes = attr.ib()
 
-    @property
     def _rref(self):
         rref = self._get_rref()
         # rref provides foolscap.ipb.IRemoteReference but in practice it is a
@@ -172,13 +195,16 @@ class ZKAPAuthorizerStorageClient(object):
             in self._get_passes(message.encode("utf-8"), count)
         )
 
-    def get_version(self):
-        return self._rref.callRemote(
+    @with_rref
+    def get_version(self, rref):
+        return rref.callRemote(
             "get_version",
         )
 
+    @with_rref
     def allocate_buckets(
             self,
+            rref,
             storage_index,
             renew_secret,
             cancel_secret,
@@ -186,9 +212,6 @@ class ZKAPAuthorizerStorageClient(object):
             allocated_size,
             canary,
     ):
-        # XXX _rref is a property and reading it does some stuff that needs to
-        # happen before we get passes.  Read it eagerly here.  Blech.
-        rref = self._rref
         message = allocate_buckets_message(storage_index)
         num_passes = required_passes(self._pass_value, [allocated_size] * len(sharenums))
         return call_with_passes(
@@ -206,78 +229,90 @@ class ZKAPAuthorizerStorageClient(object):
             partial(self._get_encoded_passes, message),
         )
 
+    @with_rref
     def get_buckets(
             self,
+            rref,
             storage_index,
     ):
-        return self._rref.callRemote(
+        return rref.callRemote(
             "get_buckets",
             storage_index,
         )
 
     @inlineCallbacks
+    @with_rref
     def add_lease(
             self,
+            rref,
             storage_index,
             renew_secret,
             cancel_secret,
     ):
-        share_sizes = (yield self._rref.callRemote(
+        share_sizes = (yield rref.callRemote(
             "share_sizes",
             storage_index,
             None,
         )).values()
         num_passes = required_passes(self._pass_value, share_sizes)
-        # print("Adding lease to {!r} with sizes {} with {} passes".format(
-        #     storage_index,
-        #     share_sizes,
-        #     num_passes,
-        # ))
-        returnValue((
-            yield self._rref.callRemote(
+
+        result = yield call_with_passes(
+            lambda passes: rref.callRemote(
                 "add_lease",
-                self._get_encoded_passes(add_lease_message(storage_index), num_passes),
+                passes,
                 storage_index,
                 renew_secret,
                 cancel_secret,
-            )
-        ))
+            ),
+            num_passes,
+            partial(self._get_encoded_passes, add_lease_message(storage_index)),
+        )
+        returnValue(result)
 
     @inlineCallbacks
+    @with_rref
     def renew_lease(
             self,
+            rref,
             storage_index,
             renew_secret,
     ):
-        share_sizes = (yield self._rref.callRemote(
+        share_sizes = (yield rref.callRemote(
             "share_sizes",
             storage_index,
             None,
         )).values()
         num_passes = required_passes(self._pass_value, share_sizes)
-        returnValue((
-            yield self._rref.callRemote(
+
+        result = yield call_with_passes(
+            lambda passes: rref.callRemote(
                 "renew_lease",
-                self._get_encoded_passes(renew_lease_message(storage_index), num_passes),
+                passes,
                 storage_index,
                 renew_secret,
-            )
-        ))
+            ),
+            num_passes,
+            partial(self._get_encoded_passes, renew_lease_message(storage_index)),
+        )
+        returnValue(result)
 
-    def stat_shares(self, storage_indexes):
-        return self._rref.callRemote(
+    @with_rref
+    def stat_shares(self, rref, storage_indexes):
+        return rref.callRemote(
             "stat_shares",
             storage_indexes,
         )
 
+    @with_rref
     def advise_corrupt_share(
             self,
+            rref,
             share_type,
             storage_index,
             shnum,
             reason,
     ):
-        return self._rref.callRemote(
+        return rref.callRemote(
             "advise_corrupt_share",
             share_type,
             storage_index,
@@ -286,15 +321,17 @@ class ZKAPAuthorizerStorageClient(object):
         )
 
     @inlineCallbacks
+    @with_rref
     def slot_testv_and_readv_and_writev(
             self,
+            rref,
             storage_index,
             secrets,
             tw_vectors,
             r_vector,
     ):
-        # Non-write operations on slots are free.
-        passes = []
+        # Read operations are free.
+        num_passes = 0
 
         if has_writes(tw_vectors):
             # When performing writes, if we're increasing the storage
@@ -306,43 +343,44 @@ class ZKAPAuthorizerStorageClient(object):
             # on the storage server that will give us a really good estimate
             # of the current size of all of the specified shares (keys of
             # tw_vectors).
-            current_sizes = yield self._rref.callRemote(
+            current_sizes = yield rref.callRemote(
                 "share_sizes",
                 storage_index,
                 set(tw_vectors),
             )
             # Determine the cost of the new storage for the operation.
-            required_new_passes = get_required_new_passes_for_mutable_write(
+            num_passes = get_required_new_passes_for_mutable_write(
                 self._pass_value,
                 current_sizes,
                 tw_vectors,
             )
-            # Prepare to pay it.
-            if required_new_passes:
-                passes = self._get_encoded_passes(
-                    slot_testv_and_readv_and_writev_message(storage_index),
-                    required_new_passes,
-                )
-
-        # Perform the operation with the passes we determined are required.
-        returnValue((
-            yield self._rref.callRemote(
+
+        result = yield call_with_passes(
+            lambda passes: rref.callRemote(
                 "slot_testv_and_readv_and_writev",
                 passes,
                 storage_index,
                 secrets,
                 tw_vectors,
                 r_vector,
-            )
-        ))
+            ),
+            num_passes,
+            partial(
+                self._get_encoded_passes,
+                slot_testv_and_readv_and_writev_message(storage_index),
+            ),
+        )
+        returnValue(result)
 
+    @with_rref
     def slot_readv(
             self,
+            rref,
             storage_index,
             shares,
             r_vector,
     ):
-        return self._rref.callRemote(
+        return rref.callRemote(
             "slot_readv",
             storage_index,
             shares,
diff --git a/src/_zkapauthorizer/tests/test_storage_client.py b/src/_zkapauthorizer/tests/test_storage_client.py
index aea80a5d5bfa7401707856b99936653dd14c8ac0..77018c2928f1c7cd991093ed416e12c5ce3a4dff 100644
--- a/src/_zkapauthorizer/tests/test_storage_client.py
+++ b/src/_zkapauthorizer/tests/test_storage_client.py
@@ -49,6 +49,10 @@ from twisted.internet.defer import (
     fail,
 )
 
+from ..api import (
+    MorePassesRequired,
+)
+
 from .._storage_client import (
     call_with_passes,
 )
@@ -174,3 +178,38 @@ class CallWithPassesTests(TestCase):
             ),
             succeeded(Always()),
         )
+
+    @given(pass_counts())
+    def test_pass_through_too_few_passes(self, num_passes):
+        """
+        ``call_with_passes`` lets ``MorePassesRequired`` propagate through it if
+        no passes have been marked as invalid.  This happens if all passes
+        given were valid but too fewer were given.
+        """
+        passes = pass_factory()
+
+        def reject_passes(passes):
+            _ValidationResult(
+                valid=range(len(passes)),
+                signature_check_failed=[],
+            ).raise_for(len(passes) + 1)
+
+        self.assertThat(
+            call_with_passes(
+                reject_passes,
+                num_passes,
+                passes.get,
+            ),
+            failed(
+                AfterPreprocessing(
+                    lambda f: f.value,
+                    Equals(
+                        MorePassesRequired(
+                            valid_count=num_passes,
+                            required_count=num_passes + 1,
+                            signature_check_failed=[],
+                        ),
+                    ),
+                ),
+            ),
+        )
diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py
index 3ffca8ae76d7b3e8ec69b5742e271a53f4b6cdab..bb79fb25e919110d0454595fe55cb81203dfbac9 100644
--- a/src/_zkapauthorizer/tests/test_storage_protocol.py
+++ b/src/_zkapauthorizer/tests/test_storage_protocol.py
@@ -921,7 +921,7 @@ class ShareTests(TestCase):
         # The nice Python API doesn't let you do this so we drop down to
         # the layer below.  We also use positional arguments because they
         # transit the network differently from keyword arguments.  Yay.
-        d = self.client._rref.callRemote(
+        d = self.local_remote_server.callRemote(
             "slot_testv_and_readv_and_writev",
             # passes
             self.client._get_encoded_passes(