Cover photo

Smart Transactions - Future of Smart Wallets

Rules, Conditions and Outcome Based Transactions

Blockchain transactions today are simple.

Arguably even stuck in a “command line interface” way of thinking.

You sign in real-time, broadcast to a specific network, interact with a target address and hope it gets executed. An experience ultimately designed for and by developers.

It’s pretty much what you would expect if you interacted with blockchains using a CLI.

Wallets take a CLI based approach for signing and broadcasting transactions

It's not possible to embed rules and conditions into the calldata for how the transaction should be executed.

Transactions themselves have no inherit programmability.

And transactions certainly aren’t abstracted from the execution layer.

In other words, you can’t easily define the “outcome” you want and in-turn let computers do what they do best - finding the most optimal route for execution.

If we are indeed building a world computer, I think we need to change that. We need transactions to be programmable and abstracted away from the execution layer. In short, transactions that can adapt to environmental changes.

As I mentioned above, transactions themselves have no inherit programability, aside from the nonce, which enforcers execution order, and therefore can actually be thought of as a very rudimentary rule for transactions.

But we want more. We want transactions to be constrained by any onchain state.

What is a Smart Transaction?

A rules, conditions and outcome based transaction.

Rule/Conditions: Constraints for when, where and how a transaction can be executed.

Outcome: If a transaction is abstracted from the execution, final state conditions must be satisfied.

And you find a prototype of smart transactions at the District Labs Github.

What's the catch though?

Smart transactions don't share the same format as a default transactions.

Instead, smart transactions are crafted and signed using the EIP712 standard - or future equivalent.

Allowing users to compose together a transaction with unique rules, conditions and outcomes.

How?

Transaction Modules

Since we can't natively add rules and conditions or express desired outcomes inside a standard EVM transaction, we need another way to inject those constraints into the calldata.

And how can we do that?

Transaction Modules.

Instead of relying on a protocol to enforce transaction constraints, like when a transaction can be executed, which is the case when swapping on Uniswap (adding a blockNumber limit to prevent a transaction being executed after X amount of time) transaction modules can introduce these constraints, but at the account level and instead of only at the protocol level.

I'll cover why that's important shortly, but to start let's first start with the how.

Unlike ERC-6900 where "modules" are installed/enabled on a smart account, transaction modules are referenced during the transaction signing process and the calldata is evaluated at runtime.

Below is a basic graphic of what that looks.

In other words, instead of installing modules on a smart account, for example an ERC20 Allowance Module, which could limit how much an authorized account can spend each month, the transaction modules are referenced via their address, alongside any necessary calldata inside of an EIP712 data structure.

If that doesn't make sense, don't worry. It's kind of a strange concept and not a widely used pattern outside of the https://github.com/delegatable framework.

But to be clear EIP-6900 and smart transactions are complimentary - not competitive.

In fact it's very likely that smart transaction will be enabled using the EIP-6900 module standard. The graphic simply illustrates the differences between module types: one type is installed on a smart account and the other is referenced during transaction signing.

So what does this look like in practice?

A smart transaction demo is available app.districtfinance.io.

Above is a screenshot of the "Limit Order" strategy interface and related smart transaction signature request, which is generated after filling in the parameters for a testnet token swap.

As you can see the transaction we're signing looks very different from a normal transaction.

It's a collection of transaction modules references, contained with an EIP712 data structure. And each module is responsible for "constraining" a specific part of the transaction or "enforcing" a desired outcome.

The smart transaction we're looking at references two transaction modules:

  • TimestrampRange

  • ERC20Swap

The TimetampRange module enforcers when the transaction can be executed.

And the ERC20Swap enforces a token swap, without actually caring about how it's executed.

Timestamp Range Transaction Module

The TimestampRange transaction module is actually fairly simplistic, even though the bytecode makes it a little intimidating first. So let's break it down piece

The TimestampRange module calldata in the screenshot above contains two timestamps.

0x00000000000000000000000000000000000000000000000000000000659f05c80000000000000000000000000000000000000000000000000000000065aae348

Min Timestamp (minTimestamp)

00000000000000000000000000000000000000000000000000000000659f05c8

Max Timestamp (maxTimestamp)

0000000000000000000000000000000000000000000000000000000065aae348

Both the minTimestamp and the maxTimestamp calldata will be evaluated at runtime i.e. when the transaction is being executed onchain.

function execute(Intent calldata intent)
        external
        view
        override
        validIntentRoot(intent)
        validIntentTarget(intent)
        returns (bool)
    {
        (uint128 minTimestamp, uint128 maxTimestamp) = _decodeIntent(intent);

        if (block.timestamp > maxTimestamp) {
            revert Expired();
        } else if (block.timestamp < minTimestamp) {
            revert Early();
        }

        return true;
    }

The transaction module evaluates the calldata relative to the globally available block.timestamp variable in the EVM. If the transaction is being executed between the authorized timestamp range, than the transaction will continue to be processed.

If the transaction is being executed outside of the timestamp range, the transaction will revert.

ERC20 Swap Transaction Module

The ERC20Swap transaction module is a bit more complex.

The ERC20 Swap module calldata in the screenshot above contains four runtime variables:

  • TokenOut (selling)

  • TokenOut (buying)

  • TokenOutAmount

  • TokenInAmount

0x0000000000000000000000005fa016760ac7962d2edef1a1d9642ce787d29078000000000000000000000000c8ec2527ec26391e588e6e5329c78a9dfcee9140000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000de0b6b3a7640000

Token Out (tokenOut)

0000000000000000000000005fa016760ac7962d2edef1a1d9642ce787d29078

Token In (tokenIn)

000000000000000000000000c8ec2527ec26391e588e6e5329c78a9dfcee9140

Token Out Amount (tokenOutAmount

000000000000000000000000000000000000000000000000000000003b9aca00

Token In Amount (tokenInAmount)

0000000000000000000000000000000000000000000000000de0b6b3a7640000

The ERC20 Swap module is different from the Timestamp Range module, because instead of comparing the decoded calldata variables with onchain state, the variables are used to "enforce" an outcome instead of comparing to existing onchain state.

Below is a snippet of the module code.

function _unlock(
        Intent calldata intent,
        Hook calldata hook,
        uint256 initialTokenInBalance
    )
        internal
        returns (bool)
    {
        (address tokenOut, address tokenIn, uint256 amountOutMax, uint256 amountInMin) = _decodeIntent(intent);
        address executor = _decodeHookInstructions(hook);

        uint256 amountIn = ERC20(tokenIn).balanceOf(intent.root) - initialTokenInBalance;

        if (amountIn < amountInMin) revert InsufficientInputAmount(amountIn, amountInMin);

        bytes memory txData = abi.encodeWithSignature("transfer(address,uint256)", executor, amountOutMax);
        return executeFromRoot(tokenOut, 0, txData);
    }

It's comparing state recorded at the start of transaction module execution, to state record at the end of the transaction module execution.

Why?

Because it expects during the middle of the transaction execution a "searcher" will complete the necessary onchain actions to satisfy those requirements. In other words the execution is abstracted from the transaction.

It doesn't care how the swap is done - only that it gets done.

The full ERC20 Swap transaction module is available at the District Labs Github.

https://github.com/district-labs/smart-transactions-v0/blob/main/contracts/intentify/src/intents/ERC20LimitOrderIntent.sol

Why

As promised, let's now examine the "why" of smart transactions.

Answering essential questions like...

  • Is all of this complexity really worth it?

  • Aren't today's transactions good enough?

  • What's the long-term benefit of smart transactions?

Is all of this complexity really worth it?

First and foremost smart transactions are a radicle departure from the existing transaction primitive, but the good news is they don't replace them - they can live together harmoniously.

You only need smart transactions if you want to embed rules, conditions and outcomes within the transaction calldata. And as of today there is limited demand for that type of transaction. Applications like CoW Swap and Uniswap X embody these ideas, but they're not generalized. Nor are the rules/conditions applied at the account level. That being said, these applications do billions in volume, so that's our first hint we're moving in the right direction.

Aren't today's transactions good enough?

The core transaction primitive is great, albeit limited in its programability.

And the transaction primitive will probably be around well into the foreseeable future, but as we progress towards "mature" onchain financial markets, the cracks will likely start to show.

Sure, institutional players can always be online - ready to sign a transaction.

But that will likely not always be the case for everyday users.

Smart transactions allow users to authorize a transaction that can be broadcast at future point in time, based on certain onchain state conditions. It's an opportunity to re-imagine how users will interact with onchain protocols and automate their personal Open Finance journeys.

And even if isn't wasn't the case then outcome based transaction (i.e. intents) will already drastically reduce friction for users by moving execution complexity to the "edge" by way of recommendations, searchers and block builders.

What's the long-term benefit of smart transactions?

Blockchains are always on and always available global computers. But if we're limited in how we can interact with them (i.e. non-programmable transactions) then it begs the question...

Are we unlocking the full potential of these blockchain systems?

Only time will tell, but I think we can look too the past for inspiration.

A UNIX Based Approach to Managing Machines

What’s a good heuristic we can use to understand the potential value of smart transactions?

UNIX.

Unix is an operating system. A very successful one. It power a majority of the Internet.

And it's underpinned by the philosophy of modularity and composability.

The Unix philosophy emphasizes building simple, compact, clear, modular, and extensible code that can be easily maintained and repurposed by developers other than its creators. The Unix philosophy favors composability as opposed to monolithic design.

https://en.wikipedia.org/wiki/Unix_philosophy

  1. Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new "features".

  2. Expect the output of every program to become the input to another, as yet unknown, program. Don't clutter output with extraneous information. Avoid stringently columnar or binary input formats. Don't insist on interactive input.

  3. Design and build software, even operating systems, to be tried early, ideally within weeks. Don't hesitate to throw away the clumsy parts and rebuild them.

  4. Use tools in preference to unskilled help to lighten a programming task, even if you have to detour to build the tools and expect to throw some of them out after you've finished using them.

It was later summarized by Peter H. Salus in A Quarter-Century of Unix (1994):[1]

  • Write programs that do one thing and do it well.

  • Write programs to work together.

  • Write programs to handle text streams, because that is a universal interface.

These principles of conciseness, modularity and context sharing underpin the Internet today.

It's time-tested a pattern that's worked before to scale the Internet to billions of users.

It only makes sense to continue these patterns as we continue to evolve and grow the Internet.

Conclusion

Transactions today are simple. They have no inherit programability. You can't include rules and conditions in the calldata or easily express "intents" or outcomes.

But smart transactions offer a potential path forward.

An opportunity to re-imagine how we interact with blockchains. The the ability to craft "set it and forget it" transactions the respond in real-time to the blockchain environments as they evolve.

Smart transactions can switch from dormant to executable the instant all necessary onchain state conditions are met. Onchain state like prices, apy, tvl, volatility, etc... can all be used as conditionals as to when, where and how transactions can be executed onchain.

I don't know about you, but I find that potential very exciting. It's truly a one of kind opportunity. To take a first principle approach to scaling the blockchain user experience to millions... and possibly even billions of users.

A moment in time to shoot for the moon - 🚀 🌕

Let's get ready for lift-off!

Loading...
highlight
Collect this post to permanently own it.
Kames' Thoughts logo
Subscribe to Kames' Thoughts and never miss a post.