Enforceable Human-Readable Transactions: how to solve Bybit-like hacks

I think you may be misunderstanding the proposal in 719, let me know if the following helps.

Assumptions:

  • The contract the user is interacting cannot act on behalf of the user outside of the contract except in ways the user has explicitly allowed (via things like .approve on a token).
  • The signing device has the ability to query the blockchain trustlessly, either via a proof based light client or a state bearing client.

Process:

  1. An untrusted web application provides a DSL like the following to the attached signing device:

    Supply {data[0,64] as number / (data[64,64] as contract).decimals() as number} {(data[64,64] as contract).symbol()} (data[64,64] as address) to Aave for lending in exchange for yield.

  2. The signing device hashes the provided DSL and makes a contract call validatedDslHash(hashOfProvidedDsl) If the AAVE contract returns false, the signer errors and terminates the signing process.
  3. The signing device queries the contract provided as the second parameter asking for the decimals and symbol, and then presents the following to the user:

    Supply 1 USDC (0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) to Aave for lending in exchange for yield.

  4. The user signs the transaction, which then gets posted to the blockchain for inclusion.

Note here that the DSL is processed on the signing device, not on the blockchain, so no need to do anything with Solidity other than writing the validateDslHash function, which can just be something like:

function validateDslHash(bytes32 hash) {
	if (hash == 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef) return true
	if (hash == 0xcafebabecafebabecafebabecafebabecafebabe) return true
	return false
}

This design does require that the signing device has the ability to execute transactions against the blockchain. You can remove this requirement by constraining the DSL so it can only operate on the provided parameters, it cannot do things like resolve contract code. This limits the utility, but still makes it better than just rendering 712 transactions in the signing tool.


This proposal does require writing solidity code for constructing a complete DSL, but it would give you rich DSL completion without the signing device needing access to the blockchain.


For SAFE transactions (and multicall, other contract wallets, etc.), the DSL renderer just needs to be recursive, which is a fairly minor engineering lift in the grand scheme of things. Certainly more complicated than only doing a single layer DSL renderer, but it is far from impossible.


Regarding a “fake USDC”, for something like Uniswap, this is certainly a concern. Luckily, we can punt this problem to Uniswap to solve. They could choose to only show addresses, not symbols, or they could choose to include warning text. Either way, it is strictly equal to or better than a 712 based approach which has essentially the same constraints.

1 Like

For reference Argent wallet has an option for transaction qr for air-gapped devices

I think this has identical security impact as having the ledger generate it for you.
Lets say your device is compromised, and you have some way to check the qr code on a trusted machine.
If the transaction itself on the trusted machine is what you expect and the hashes on the trusted machine and the ledger are identical you should be safe.

Yeah, I think there’s a misunderstanding on what the proposed technique does. Let’s go back a few steps, because this is exactly the issue the technique addresses. Then we can talk on tradeoffs and feasibility.

First, let me highlight this is a programming technique that app developers could implement at the application level. This doesn’t require changing wallets nor protocols; instead developers may choose to implement their apps following this pattern.

Now, to address the core point: yes, the honest UI is supposed to query the description (or embed the same logic that generates it). Suppose, however, that the UI is dishonest. It asks the user’s wallet to sign a 712 message with malicious payload, but displays an innocuous description The user, understanding only the benign description, approves the signature. Critically, under the proposed technique, the signed transaction includes both the description and the actual calldata.

If these do not match (i.e., the description does not faithfully represent the calldata), the transaction will revert on-chain, at the smart contract level. Therefore, the technique removes the need to trust the front end: any UI spoofing attempt that tries to misrepresent the transaction will simply fail at execution.

Do you agree that, at least for this simple case, this pattern mitigates UI spoofing attacks?

I see. I think I do understand now better what you’re looking for, thank you for explaining! Ok, what you’re saying is a lot closer to making sense than I originally thought. The part I was missing was the fact that the untrusted website gives a DSL with the parameters in the DSL itself, that’s pretty cool. And then the wallet can “read” this DSL.

My issue still persists, though; I think for big, complex transactions, this still breaks down.

I’d like to see how your proposal would solve something like this. It seems to me that this would go back to the “terms of service” issue, where the description is too big and people still sign, and they don’t really know what they are signing still. Whereas if a wallet just decodes calldata “better”, that’s more readable, more reliable, and cheaper in the long run.

1 Like

Yes! Following @MicahZoltu’s newest example, I did have a misunderstanding of the proposal! It makes a lot more sense to me now! Thank you both!

I still think this isn’t the best path forward for reasons here:

1 Like

I’m curious why you think a more automated solution would result in a more readable output than one that had a human explicitly decide how to express the transaction?

For nested transactions, like with a SAFE or multisend, a DSL may look something like this:

The SAFE ({from:address}) will be executing the following functions in this order:
{extract_nested_calls_from_array(data[0], from_offsets[0], calladata_offsets[32])}

The UI would also provide a DSL for each of Aave, USDC, and Multisend contracts and the signing device would check those DSLs with their respective contracts which it gets from each array item decoded according to how the SAFE defined decoding of the batch in its DSL.

Note: Dealing with arrays of data is one of the harder problems that needs to be solved by someone taking on this project, the above is just something off the top of my head to illustrate it is possible, but I’m sure we could do better.

1 Like

than one that had a human explicitly decide how to express the transaction?

I just don’t see how it’s that much better than just decoding the calldata. Plus, since each contract specifies its own docs specifically for its contract, I can see batched contract calls becoming very complicated. Where each contract doesn’t have the context of another transaction in a batch.

I just feel like this is a lot of effort for very little gain, whereas just better calldata decoders get the job done, and essentially convey the same information. It feels like we are, in essence, encoding a translator into each transaction… And I still don’t think the translators will be able to be successful enough, especially with complex transactions.

Maybe seeing a full proof of concept for a complex transaction would work… But again, now that I understand it better, I don’t think it’s a terrible idea, but just too much work for too little gain.

No matter what, you’ll still need to get this text data off your hardware wallet, so we still have issues to solve before we do this. For example, making sure the address or amount you read off your device, it would be better to compare directly to your expected address, rather than with your eyes spot check the address on your device.

I don’t think any single solution will solve all the issues, including the one in the OP. Ultimately, I believe we’ll need multiple, layered, and ever-evolving approaches to effectively address the ongoing challenges with signing UX.

I agree that improving wallets with better decoders, tracers, or simulators is a valuable step. However, this approach has its own limitations, as @MicahZoltu has mentioned.

One significant concern is that increasing the capabilities (e.g., internet connectivity + EVM) of trusted components, like hardware wallets, expands their attack surface and reduces reliability. I’d really prefer my wallet to be a low-powered offline device.

Relying on simulating potentially malicious transactions and inspecting their effects isn’t a panacea either — an attacker could front-run your transaction and cause entirely different results. Besides needing an EVM and access to trusted chain state.

Regarding LLMs, they can be great tools, but they don’t fully address signing UX either. You’d need both a trusted model and secure hardware capable of self-hosting it. I wouldn’t trust this setup for one billion dollars.

Finally, general-purpose tools can’t match the expressiveness of application-specific descriptors — the latter has knowledge of their applications, which enables understanding the meaning of the transaction.

All that said, I agree that better decoding and simulation can certainly improve UX.

I think the DSL can work, but it’s not the layer that should use it.

Let the wallet be agnostic, and just render the bytes air-gapped to whoever wants
to read them using animated QRs.

If the calldata sender (browser wallet / HW desktop app), is compromised, anyone gets a chance to
review the bytes and use their understanding of the calldata + DSL to figure out their score of
confidence to the user.

Leave the DSL part to be consumed at a higher lvl from the read calldata from the QR.

It’s the only solution can that be retrofitted to all the hardware wallets and provide instant
improvement through firmware update that prevents the Bybit hack.

It doesn’t fix frontrunning, nothing ever will.

The DSL is most often enough for simple cases, but imagine it has to digest these:

[etherscan|io] /tx/0xc490b94fc7bb88ae918c191fe4328a34ee97fab310916f0e07a72c340ce6efe0

You can always roundtrip a user tx all around and get obfuscated traces. This is why we warn about tx.origin. Specially with 7702, try giving me eth I’ll emit 1000 events and do 32 swaps for 1 wei to mess with your decoder.

But now that’s the decoder problem, not the hardware wallet one.
And when I say decoder I mean decoderS, feel free to run multiple in parallel from the same QR code.
Feel free to assume 1… 2… 12 of your decoders are compromised.

You can offload that trust to whoever you want.
You decide when you’ve seen enough to feel confident to give that tx the final go and agree to sign on the HW.

So

  • Any external device (e.g. phone app, tablet, hardened reader) can decode the transaction offline using arbitrary logic, including DSL interpreters, LLMs, or known heuristics.
    In the LLM case the use can choose the

  • The wallet remains agnostic: its job is not to interpret, only to output the signed bytes.

  • The end user, through their delegated tooling, chooses when the decoded result inspires enough confidence to proceed with signing.


Most hardware wallets already possess the display resolution required for that rendering.
Even if their current firmware does not expose direct QR animation features, a simple firmware update would suffice to enable compatibility.
We expect that once a minimal standard is proposed, most vendors will follow, not because it’s technically hard, but because users will demand it.

It would also give independent decoders a chance to catch these:
[x|com] /khalo_0x/status/1929900274182730226

1 Like

This transaction looks like one crafted by a trading or MEV bot, not something a human is looking at. We don’t need to do anything to improve readability of such transactions, because there is no human in the loop.

Offline wallets can certainly do a better job at data presentation today, without any changes to standards. I fully encourage them to do so, and I have been trying to get AirGap.it to improve their rendering by just decoding calldata alone. However, this only meaningfully helps advanced users who understand what is going on at a low level, and doesn’t really do much to help novice users who don’t deeply understand how contracts work. To help those users we need trustless hand crafted strings presented to them, like how this proposal or EIP-719 suggests.

It sounds like what you want is for signing tools to do a better job of presenting readable transactions using data that is available to them now, which I fully endorse/support.

1 Like

not something a human is looking at.

You missed my point about 7702 then. Let’s say your tx calls a contract to reimburse me. (idk I staked for you or something in the past that ties us together). And now you want to pay me back, using the good old protocol you know safe.

But this is 7702 era, and inbetween lending and repaying my EOA is now a SC.

So when you trigger the tx → lendingprotol::repay → me::receive() assuming your estimation of gas, and my 63/64 being “generous”. I can now try to pollute your call, by doing stuff when im now in control in the receive. I have tokens to trade that were scheduled? You made them start, thanks! You became my personal Gelato Protocol!

And now… you kinda have to show that to the human at some point… So if

We don’t need to do anything to improve readability of such transactions
readability on these doesn’t matter.

Then what does?

Human readability of transactions that humans should be reading. Bot transactions aren’t read by a human, so we can ignore them in conversations about “how do we improve the situation around informed signing”.

Contracts should not be calling into untrusted code as part of a contract execution. Use a mailbox if necessary. This has been true for some time, as people have had contract wallets for almost as long as Ethereum has existed. 7702 just makes it so EOAs can also be contract wallets, but that doesn’t change any programming paradigms.

Either way, this seems to be veering far afield from the proposal this thread is about, so we should probably circle back to that.

1 Like

You don’t get it. Please read my post until you understand we don’t control this path until we get the PAY opcode that doesn’t trigger sideeffects on paying an EOA (that can now be a contract).

You caught up a bit on 7702 when you tried to help build a rescue code last week but the fact that you don’t understand that part makes me think you don’t have the full picture on it.

1 Like

Good critique. You’re pointing at real failure modes in UI-generated descriptions and on-chain description functions. Let me show how ERC-8001 avoids both.

The Problems You Identified

  1. UI-generated descriptions: User trusts UI to accurately describe what they’re signing

  2. Onchain description functions with external calls: symbol(), decimals() can be spoofed; cross-contract lookups become intractable for multi-call flows

  3. No unified semantic source: Safe, Aave, USDC, Multisend each have partial views—they can’t describe the full bundle

These are valid. But they have objections to deriving meaning after the transaction is constructed. ERC-8001 takes a different approach.

What ERC-8001 Signs Instead

{
  "intentType": "supply",
  "asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  "chainId": 1,
  "rawAmount": "1000000",
  "protocol": "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9",
  "constraints": {
    "noAdditionalApprovals": true,
    "noDownstreamCalls": true,
    "expiry": 1704067200
  }
}

The user signs this object. Not calldata. Not a UI-generated string.

How This Avoids the Problems

No runtime metadata lookups: The intent binds to the raw amount and address. The wallet can render “1 USDC” as a convenience label if it has a trusted mapping. But the enforceable meaning is 0xA0b8..., 1,000,000 units, chain 1.

No UI as source of truth: The UI proposes the intent object, but the wallet verifies the structure and displays the bound addresses. The user authorises the object, not a description.

No per-contract description functions: ERC-8001 doesn’t ask Safe, Aave, or USDC to describe anything. The intent is declared at the application layer and verified against execution. Contracts execute; they don’t interpret.

For Multi-Call Flows

{
  "intentType": "sequentialActions",
  "actions": [
    { "type": "approve", "token": "0xA0b8...", "spender": "0x7d27...", "maxAmount": "1000000" },
    { "type": "supply", "protocol": "0x7d27...", "asset": "0xA0b8...", "amount": "1000000" }
  ],
  "constraints": {
    "noOtherApprovals": true,
    "noOtherCalls": true
  }
}

The semantics are declared up front. The wallet doesn’t infer from calldata. The contracts don’t describe themselves. The verifier checks that execution stayed within the intent’s declared bounds.

In closing

Your objection is strong against:

  • UI-decoded descriptions as truth

  • DSLs with untrusted metadata lookups

  • Per-contract description functions

ERC-8001’s answer: don’t derive meaning from execution. Declare the meaning in the signed object.

The wallet renders the intent for human readability. The verifier proves execution matches the intent. No external calls to untrusted contracts. No cross-contract description coordination. The semantics are bound into the authorisation itself.

Thinking about this again. I think my terms of service argument is weaker than I originally thought.

I think this will be a terms of service issue, and we can have 8213 AND something like this.

I still think signing 2 transactions is a bit silly. It should always be in one transaction. With 7702 this is more possible to be in a single transaction. I still feel like it’s redundant gas tho, and I think a user doesn’t need to sign the DSL data. But I feel like this could be tweaked and be a self-documentation alternate to 7730.

7730: Community curated list
This ERC: self-documentation ERC (fallback if 7730 doesn’t support the protocol)

1 Like

this “intent” idea looks like setting up a weak language - relative to what is used to write the underlying contracts - so you can do formal validation reliably. and then reliable translation to natural language. fair enough. your example has a set of intentTypes and constraints. there is then i guess some scheme that maps intentType x constraint → observable invariants or whatever things you can evaluate with natural language descriptions.

thats fine. but - and this is i think the tos issue @PatrickAlphaC raises - you need to do something about what lives inside the token, protocol etc contracts that sit inside your intent statements. this is recursive. you need to either:

  1. ensure those are also weak enough that formal validation is possible
  2. trust a mapping into something like your weaker language for validation
  3. reject the tx, or describe it as “unknown”

3 is the tos issue. as an example anything with a code path that touches a safe with an module that hasn’t has 1 or 2 done falls into 3. for big protocols there’s gonna be a lot of 3 as the code in “protocol” is effectively everything the interaction might touch.

does blocking every tx in a token with an admin multisig that has weird modules added sound like a good idea? probably! but it is also an obvious usability issue given how common variants of that are.

The transaction needs to include hash of user description. Then AI can match the description to what transaction does during signing.