Reserve Balance
Summary
In Monad, asynchronous execution means
that nodes achieve consensus on a block proposal prior to executing the transactions in
that block. Execution is required to be completed in the next k
(delay factor) blocks.
Because consensus operates on a k
-block delayed view of the global state, it is necessary
to adjust the consensus and execution rules slightly to allow consensus to safely build
and validate blocks that include only transactions whose gas costs can be paid for.
Monad introduces the Reserve Balance mechanism to allow consensus and execution to collaborate across a multi-block lag to ensure that all EOAs must have enough MON in their account to pay for gas for any transaction included in the blockchain.
The Reserve Balance mechanism places light restrictions on when transactions can be included at consensus time, and imposes some conditions under which transactions will revert at execution time. These are described in greater detail herein, and more formally in Section 6 of the Monad Initial Spec proposal from Category Labs.
The Reserve Balance mechanism is designed to preserve safety under asynchronous execution without interfering with normal usage patterns. Most users and developers need not worry about the Reserve Balance constraints, however we provide the details here for those encountering corner cases.
The Reserve Balance rules also allow Monad to support EIP-7702.
Here is a very brief summary of the rules:
- Execution time: during execution, transactions revert due to spending of account balance
(outside of gas payments) when that account balance dips below a specified reserve balance
level. An exception is made for undelegated accounts that have no pending transactions within
the past
k
blocks. - Consensus time: When performing block validity checks for block
n
, consensus queries account balances of transaction senders from execution state as of blockn − k
and checks that the worst-case balance for each sender after executing blocksn − k + 1
through blockn
will be non-negative when factoring in execution spending restrictions.
Throughout this document, an inflight transaction refers to a transaction that has
been included in a block less than k
blocks ago, i.e. which is not yet reflected in
the delayed state.
Why is reserve balance needed?
Monad has asynchronous execution: consensus is allowed to progress with building and
validating blocks without waiting for execution to catch up. Specifically, proposing
and validating consensus block n
only requires knowledge of the state obtained after
applying block n-k
, where k
is a protocol parameter currently set to 3.
While asynchronous execution has performance benefits, it introduces a novel challenge: how is consensus supposed to know the validity of a block if it does not have the latest state?
Let’s illustrate this challenge with an example (for our examples, we will use k = 3
):
Consensus is validating block 4, which contains a transaction t
from Alice with the
relevant fields as:
sender=Alice, to=Bob, value=100, gas=1
Consensus only has the state that was obtained by executing block 1:
block=1, balances={Alice: 110}
If consensus simply accepts block 4 as valid because Alice appears to have enough
balance, it risks a safety failure. For instance, Alice may have already spent her
balance in transaction t’
in block 2. This creates a denial-of-service (DoS)
vector, as Alice could cause consensus to include many transactions for free.
First attempt at a solution
One idea is for the consensus client to statically inspect transactions in blocks 2
and later, checking if Alice has spent any value in her transactions. This would let
consensus reject block 4 as invalid if any transaction before t
(such as t'
) in
blocks 2, 3, or 4 originates from Alice and spends some value or gas.
While this is a fine solution on the face of it, it suffers from two shortcomings:
-
Suppose, as part of smart contract execution in blocks 2 or 3, Alice received a lot of currency. She would have had enough balance to pay for transaction
t
despitet'
existing, if only we had the latest state. So, rejecting transactions based solely on static checks is overly restrictive. -
It is not only restrictive, it is also not safe with EIP-7702. With EIP-7702, Alice could have her account delegated to a smart contract, which can transfer out currency from Alice’s account in a way that is not statically inspectable by consensus. Concretely in our example, Alice does not need to send a transaction like
t'
from her account in order to spend currency from her account, if her account is delegated. A spend could potentially be triggered by a transaction submitted by anyone else. So our static check would not succeed and it may be unsafe to accept block 4 as valid even if we don’t see any other transaction from Alice in blocks 2, 3 and 4.
Reserve balance as the solution
Simple version
Intuitively, the core idea of reserve balance is as follows: if consensus and execution agree ahead of time that, for each EOA, execution will prevent the account balance from dropping below a certain pre-determined threshold known to consensus, then consensus can then safely include transactions whose gas expenditures stay below that threshold, without knowing the latest state and without being vulnerable to the DoS vector described above.
In our example, if execution ensures that Alice’s account cannot be drawn below
10 MON (otherwise, the withdrawing transactions are reverted), then consensus can
safely include transaction t
, as by definition Alice’s account will have at least
10 MON to pay for transaction t
.
This concept can be generalized as follows:
- Consensus accepts transactions from user
u
after the delayed states
as long as the sum of the gas fees for all inflight transactions sent byu
is below a parameter calleduser_reserve_balance
. - Execution reverts any transaction that causes an account’s balance to dip below
user_reserve_balance
, except due to transaction fees.
In Monad, user_reserve_balance
is currently set to 10 MON
for each EOA.
An additional refinement to improve UX
One criticism of the above rule is that it is difficult for users with balances below the reserve to do anything that requires MON other than for gas fees.
For instance, the following behaviors might be desired, but are currently blocked
by the above rule (with user_reserve_balance
set to 10 MON
):
- Alice has a balance of 5 MON and wants to send 4.99 MON to Bob (plus pay 0.01 MON in gas)
- Alice has a balance of 20 MON and wants to swap 18 MON into a memecoin (plus pay 0.01 MON in gas)
To address this, we add some additional conditions where transactions are allowed.
First let's define an "emptying transaction":
An "emptying transaction" is a transaction that (when evaluated at time of execution) could take the balance below the reserve balance.
Notice that if a user account is not EIP-7702-delegated, then consensus can simply inspect transactions statically in order to estimate the lowest a user’s balance can possibly go (since an undelegated user’s account can only be debited due to value transfers and gas fees specified in the transaction data).
Therefore, we add the following rule:
- Execution policy: for each undelegated account sender, if a transaction is the first inflight transaction from that sender, and the transaction would have otherwise reverted due to being an "emptying transaction", allow that transaction to proceed anyway.
- Consensus policy: for each undelegated account sender, if a transaction is the
first inflight transaction from that sender, then statically inspect that transaction's
total MON needs (i.e.
gas_bid * gas_limit + value
), and - if this will end up being an "emptying transaction" - take into account the fact that execution will still allow this transaction through. This means that for any subsequent transactions in the nextk
blocks, the reserve balance that consensus is working with will be lower.
This rule lets execution allow undelegated accounts to dip below the reserve balance
once every k
blocks. Since k
blocks is 1.2 seconds, this policy should allow most small
accounts to still interact with the blockchain normally.
The additional policy allows both of the examples mentioned at the start of this section,
as long as they are the first transaction sent by the sender in k
blocks.
Full specification
See the reserve balance spec for the formal set of Reserve Balance rules.
Algorithms 1 and 2 implement this check for consensus and execution, respectively.
Algorithm 3 implements the mechanism to detect the dipping into the reserve balance (Algorithm 2 uses Algorithm 3 to revert transactions that dip).
Algorithm 4 specifies the criteria for emptying transactions:
- The sender account must be undelegated in the prior
k
blocks. This is checked statically by verifying the account was undelegated in a known state in the pastk
blocks, and there has been no change in its delegation status in the lastk
blocks (this can be inspected statically). - There must not be another transaction from the same sender in the prior k blocks.
Here is a quick summary of the reserve balance rules at consensus time:
If the account is not delegated and there are no inflight transactions
If the account is not delegated, and there are no previous inflight transactions, then consensus checks that the gas fee for this transaction is less than the balance from the lagged state.
If the account is not delegated and has one emptying inflight transaction
If the account is not delegated, and there is one previous inflight transaction, then
consensus has to take into account the inflight transaction's total MON expenditures
(including value
):
A new transaction can only be included if the sum of all inflight transactions' gas fees (excluding the first one) is less than the reserve:
All other cases
The reserve is equal to minimum of systemwide reserve balance (10 MON
) or the
account's balance at block n - k
:
A new transaction can only be included if the sum of all inflight transactions' gas fees is less than the reserve:
Adjusting the reserve balance
The reserve balance is currently the same for every account (10 MON
).
In a future version, the protocol could allow users, through a stateful precompile, to
customize their reserve balance.
Coq proofs
The safety of the reserve balance specification has been formally proved in Coq.
The full proofs documentation is available here.
The consensus check is formalized in Coq as consensusAcceptableTxs
. The predicate,
consensusAcceptableTxs s ltx
, defines the criteria for the consensus module to accept
the list of transactions ltx
on top of state s
.
The proof shows that consensusAcceptableTxs s ltx
implies that when the execution
module executes all the transactions in ltx one by one on top of s, none of them will
fail due to having insufficient balance to cover gas fees. The proof is by induction
on the list ltx
: one can think of this as doing natural induction on the length of ltx
.
The proof in the inductive step involves unfolding the definitions of the consensus
and execution checks and considering all the cases. In each case, the estimates of
effective reserve balance in consensus checks is shown to be conservative with respect
to what happens in execution.
Additional examples
To test your understanding, here are some examples along with the expected outcome. Each example is independent.
In the following examples, we use start_block = 2
, meaning the initial balances
and reserves are after block 1. We also specify the reserve balance parameter for
each example, although it is a constant system wide parameter.
For each transaction, the expected result is indicated by a code:
- 2: Successfully executed
- 1: Included but reverted during execution (due to reserve balance dip)
- 0: Excluded by consensus
Example 1: Basic transaction inclusion
Initial state:
Alice: balance = 100, reserve = 10Bob: balance = 5, reserve = 10
Transactions:
Block 2: [ Alice: send 1 MON, fee 0.05 — Expected: 2 Bob: send 2 MON, fee 0.05 — Expected: 2]
Final balances:
Alice: 98.95Bob: 2.95
Example 2: Low reserve balance but high balance
Initial state:
Alice: balance = 100, reserve = 1
Transactions:
Block 2: [ Alice: send 3 MON, fee 2 — Expected: 2 (emptying transaction) Alice: send 3 MON, fee 2 — Expected: 0 (excluded)]
Final balance:
Alice: 95.0
Example 3: Multi-block, low reserve but high balance
Initial state:
Alice: balance = 100, reserve = 1
Transactions:
Block 2: [ Alice: send 3 MON, fee 2 — Expected: 2]
Block 5: [ Alice: send 3 MON, fee 2 — Expected: 2]
Final balance:
Alice: 90.0
Example 4: Comprehensive
Initial state:
Alice: balance = 100, reserve = 1
Transactions:
Block 2: [ Alice: send 99 MON, fee 0.1 — Expected: 2 (large emptying transaction)]
Block 3: [ Alice: send 0.5 MON, fee 0.99 — Expected: 0 (excluded)]
Block 4: [ Alice: send 0.8 MON, fee 0.1 — Expected: 1 (included but reverted)]
Block 5: [ Alice: send 0 MON, fee 0.9 — Expected: 0 (excluded) Alice: send 5 MON, fee 0.1 — Expected: 1 (included but reverted) Alice: send 5 MON, fee 0.8 — Expected: 0 (excluded)]
Final balance:
Alice: 0.70
Example 5: Edge case — zero value transactions
Initial state:
Alice: balance = 2, reserve = 1
Transactions:
Block 2: [ Alice: send 0 MON, fee 0.5 — Expected: 2 Alice: send 0 MON, fee 0.6 — Expected: 2 Alice: send 0 MON, fee 0.5 — Expected: 0 (exceeds reserve)]
Final balance:
Alice: 0.9
Example 6: Reserve bBalance boundary
Initial state:
Alice: balance = 10, reserve = 2
Transactions:
Block 2: [ Alice: send 1 MON, fee 2 — Expected: 2 (matches reserve) Alice: send 0 MON, fee 0.01 — Expected: 2]
Final balance:
Alice: 6.99