· 10 min read

Sign the Outcome, Not the Steps: Why Batch Transactions Need Post-Conditions

Sign the Outcome, Not the Steps: Why Batch Transactions Need Post-Conditions

You click a button in a DeFi app. A wallet pops up. It asks for your signature. Inside the popup:

You confirm. You always confirm. Everyone always confirms.

This is the dirty secret of on-chain UX: almost nobody actually reads what they're signing. Not because users are careless, but because what wallets show them isn't readable in any meaningful sense. It's a hash, a selector, a raw call. Decoding it properly would require knowing the ABI of every contract in the call graph, simulating the transaction, tracking token flows, and cross-referencing them with your intent.

That's not a thing a human does while their coffee is still hot.

Batches make it dramatically worse

Single transactions are already hard to read. Batch transactions — one signature that triggers a sequence of calls — are a readability catastrophe.

A typical DeFi batch today might look like:

  1. Approve Router for USDC
  2. Approve Router for WETH
  3. Swap USDC → WETH on some pool
  4. Wrap more ETH
  5. Deposit into a vault
  6. Stake the vault shares
  7. Claim rewards
  8. Send rewards to another address

The wallet shows you eight opaque steps. Each one has its own target, its own parameters, its own potential for mischief. The intent — "I want to end up with my position deposited and staked, and I'm okay giving up some USDC and WETH to get there" — is nowhere on screen.

And here's the really dangerous part: even if you read step 1 carefully, there is nothing tying the honest description of the batch to its actual effect. A malicious frontend can ask you to sign a batch whose readable label says "stake into Aave" while the underlying calls drain your wallet through a freshly-funded router you've never seen before.

And the signing environments that matter most for large amounts — hardware wallets, airgapped signers, cold storage — can't help at all. A Ledger, a Keystone, a paper-QR signer, an HSM: none of them have an RPC, a simulator, or a token-flow differ. They can't ask "what would this transaction actually do?" They can only display what's in the bytes you handed them.

When those bytes are a 30-call batch with dynamically-resolved calldata, the device has nothing meaningful to show. It renders selectors and hex and hopes you recognize them. At the exact moment the most valuable keys in the system are asked to sign, the user is forced to sign in the dark.

This is the core asymmetry: the signing device that needs readability the most is the one that has the least context to produce it. No amount of better simulation in the hot wallet fixes the cold one. The invariant has to live somewhere the offline device can actually see it — which means it has to live inside the signed payload itself.

Verifying every step is the wrong question

The usual response to this problem is: make the steps readable. Better decoders. Better simulators. Better tooltips. All of these help, but they share a common limitation: they try to explain what happens inside the transaction, step by step.

That's the wrong question to put to a user.

Users don't care about the middle. They care about the endings. They care about what goes in and what comes out:

"I'm willing to spend up to 1,000 USDC, and I expect to end up with at least 0.35 WETH and my Aave position unchanged."

That sentence is something a user can actually read, understand, and approve. Everything else — which router, which pool, which hop, which slippage — is plumbing.

So the real question is: how do we let users sign the outcome instead of the plumbing?

Post-conditions: signing the ending, not the middle

post-condition is an assertion about the state of your account after a transaction finishes. It is the on-chain equivalent of saying:

"I don't care what this transaction does, as long as when it's done:my USDC balance is at least 1,000my WETH balance is at least 10,000my Aave position hasn't shrunk"

If any of those invariants are violated when the transaction settles, the whole thing reverts. Atomically. No partial execution, no "oops the last step failed after we already spent your USDC."

Post-conditions flip the signing model:

Without post-conditionsWith post-conditions
User audits each step (and fails)User audits the outcome (and succeeds)
Dapp is trusted to behave correctlyChain enforces the outcome
Bug in the dapp → user loses moneyBug in the dapp → transaction reverts
Readability scales with complexityReadability is constant

The big win isn't just safety. It's cognitive compression. A batch with 3 steps and a batch with 30 steps look the same on the signing screen: two lines, one human-readable invariant, one Confirm button you can actually mean.

EIP-8211 makes this a first-class feature

EIP-8211 — Smart Batching is a proposed Ethereum standard for batch transactions that resolve and validate their parameters at execution time, on-chain. Most of the proposal is aimed at composing complex DeFi flows (feeding the output of one step into the input of the next, handling slippage, coordinating across chains), but buried in the same mechanism is the primitive we've been looking for: predicate entries.

A predicate entry is a step in a batch that doesn't call anything. It just reads on-chain state and asserts conditions on it. If any assertion fails, the whole batch reverts.

The relevant building blocks are:

Put them together and a post-condition is just a small, declarative piece of data at the end of a batch:

ASSERT balanceOf(USDC, me)  >= 1000 * 10^6
ASSERT balanceOf(WETH, me)  >= 10000 * 10^18

No new contract. No extra opcode. No off-chain predicate service. The same encoding the batch already uses for its real steps is re-used for the invariant check. The chain does the rest.

The invariant lives in the signed bytes

This is the property that makes post-conditions work for hardware wallets and airgapped signers, and it's worth stating plainly:

A post-condition is not a tooltip. It is not a UI hint. It is a predicate entry in the batch itself, encoded in the same bytes the signer will sign, in a fixed, deterministic format.

That changes what a minimal signer has to do. A hardware wallet doesn't have to understand Uniswap, Aave, or any of the 30 intermediate calls. It just has to parse the handful of predicate entries at the tail of the batch — each one is a (token, account, comparator, threshold) tuple — and render them in human units:

This batch will succeed only if, when it finishes:

  USDC  balance of 0xAbc…  ≥  1,000.000000
  WETH  balance of 0xAbc…  ≥  0.350000
  aUSDC balance of 0xAbc…  ≥  3,383.000000

[ Approve ]   [ Reject ]

No RPC. No simulation. No token list lookup beyond the symbol/decimals the device already has for common ERC-20s (and for unknown tokens, the raw address and raw units, which is still honest and still readable).

Everything else in the batch — the swap hops, the router addresses, the calldata — can remain opaque on the device's screen without putting the user at risk, because none of it can affect the outcome the user signed for. Either the final state satisfies the invariants and the batch commits, or it doesn't and the whole thing reverts.

Who produces the invariant

Two modes, same on-chain mechanism:

Hot wallet, simulation-driven. When a dapp asks to send a batch, it tells the wallet what it wants to do ("swap 1 ETH to USDC via Uniswap v4, then supply to Aave"). The wallet simulates the intent, sees which balances are expected to move, derives a minimum-expectation set, and appends post-conditions automatically before handing the batch to the signer:

Cold wallet, payload-driven. The host machine (or the dapp itself) assembles the batch including the post-conditions and forwards it to the hardware wallet. The device has no network, but it doesn't need one: the invariants are already in the bytes. It displays them, the user compares them against their intent, and signs — or doesn't.

In both cases the user ends up signing the same question: "at the end of this transaction, do I still have at least X and at most Y?" The chain enforces that answer. The dapp can't lie about the result, because the result is what the user actually signed — in bytes, not in UI copy.

Why this changes the security model

Today's signing model trusts the dapp to build the right transaction, and trusts the wallet to explain what that transaction does. Both are failing, often quietly.

Post-conditions change who trusts whom:

This closes the most damaging class of wallet exploit: the bait-and- switch transaction, where the UI says one thing and the calldata does another. Under a post-condition model, a bait-and-switch transaction simply reverts. The user loses a bit of gas. Nobody loses funds.

It also incidentally fixes a long tail of boring failures:

All of these currently produce half-broken states that the user has to untangle manually. With injected post-conditions, they become clean, atomic no-ops.

What retail users should take away

You don't need to understand how EIP-8211 works. You do need to know what to demand from the wallets and dapps you use:

  1. "What will I have when this is done?" — The signing screen should answer this in plain numbers, not hex. If it doesn't, your wallet is years behind.
  2. "What happens if it's wrong?" — The only acceptable answer is "the transaction reverts, you lose gas, you keep your funds." If the answer is "I guess you have to check the explorer," the wallet is not enforcing your intent.
  3. "Who decides the minimums?" — Your wallet should. Not the dapp. Not the relayer. The wallet is the only party whose incentives are aligned with yours.

A wallet that injects post-conditions on your behalf turns every signing prompt into a contract between you and the chain: "I will sign this only if, at the end, I still have X and Y." That's a sentence you can read. That's a click you can mean.

What frontend developers should build

If you're building a wallet, a dapp, or an SDK that sits between the two, the concrete work looks like this:

Done well, this collapses the entire "is this transaction safe to sign?" problem — today a research project every time — into a single readable line the user can accept or reject.

Closing thought

The long-running joke of on-chain UX is that we've built the most powerful financial system ever designed and then asked its users to approve it one hex string at a time. Readability isn't a cosmetic issue; it's the point where the security model of the entire stack actually terminates. If the user can't understand what they're signing, it doesn't matter how rigorous the cryptography underneath is.

Post-conditions don't make transactions easier to read. They make readability unnecessary for safety. You sign the outcome. The chain enforces the outcome. Everything in the middle becomes the computer's problem — which is exactly where it belongs.

EIP-8211 happens to be one of the first concrete proposals that gives wallets a standard, composable way to inject those outcomes onto every batch they see. The sooner wallets pick it up, the sooner the signing screen stops being a leap of faith.

Further reading