Mirror recently introduced splits, a native feature that lets you route funds continuously to an unlimited number of Ethereum addresses, according to a set of percentage allocations.
But what did it take for splits to evolve from an idea to a set of efficient, production ready smart contracts on Ethereum? Read the full story in this post!
How do Mirror Splits Work?
There were three problems that we needed to solve for splits:
- Each split needs a unique address that can be used to receive funds from an economic producer (e.g. NFT auction, crowdfund, or just an EOA sending ETH). It needs be cheap to deploy and specify the split allocations.
- Splits need to allow allocations for many accounts, while being relatively cheap to distribute the funds. At first we were thinking in the range of 15-20 accounts.
- Splits need to be reusable — accounts should be able to withdraw and deposit multiple times
To solve the unique address problem, we use a simple proxy pattern that allows us to deploy a contract that only stores some storage variables and delegates all functionality to a core logic contract. This makes it cheap to have unique addresses, and is a fairly common approach in protocol design. It costs 233,315 gas to deploy a Split, which today would be about $50.
When we first set out to build our split mechanism, we tried a number of different approaches. The first one was naive: a contract with a function that efficiently sends its balance to a pre-set list of addresses cheaply, according to some allocation verified against a hash.
But what if we wanted to split across 30 addresses? This would make it more expensive to operate, since costs scale linearly with the number of allocations. And 100? At that point, the paradigm breaks down completely.
We decided to try a different approach, using some of the core cryptographic primitives of Ethereum to enable cheap splits. In the next section, we'll go over how these work, and how we integrated them into a useful new feature.
Technical Foundations: Understanding Merkle Trees
The Merkle tree is perhaps the most salient data structure in blockchain architecture. It can be used to create a very small representation of a large amount of data, while still enabling one to prove certain information about the original data in an efficient way.
To create and use a Merkle tree, we split our data into chunks, structuring them as a tree (e.g. a binary tree), and then hash the nodes together repeatedly until we get a root hash. We can use the root hash to prove whether some chunk of data was in the original tree at a given location, without needing to share all of the original data. This kind of proof is known as a Merkle Proof.
Given a public Merkle Root, we can prove that some data (e.g. a balance) exists at a given place in the tree, by verifying a Merkle Proof. For example, we can provide a proof that Hash 2 is in the given place in this tree.
A simple application of a Merkle Proof is to create a Merkle tree for a large database of balances, and publish the root. When someone wants to know what the balance is for a given address, we can return the balance and a proof, which can be verified (assuming you trust the published root) without downloading the contents of the database.
Therefore, we can prove facts of a large dataset, even when we only access a very small amount of data. Each block in Ethereum contains multiple Merkle trees to prove various aspects of Ethereum's state and state transitions.
On-chain Merkle Proofs
While Merkle Proofs have been primarily used as the foundation of core blockchain architecture, they have also found uses in applications built on top of blockchains.
Smart contracts that verify Merkle Proofs have been around for many years — for example Raiden's implementation in early 2017, and Ameen Soleimani's later port of it.
The most popular application has probably been Uniswap's 2021 airdrop of 150 million UNI to hundreds of thousands of addresses. It would have been infeasible to airdrop tokens to this many addresses without Merkle Proofs — the proofs allowed users to retroactively claim their share of the airdrop, instead of Uniswap needing to send an ERC20 transaction to each address up-front.
Integration with Splits on Mirror
With Splits, our goal was to allow someone to specify a list of funding allocations of any length, and then allow those accounts to withdraw funds according to their allocation — without costing the deployer or funder an excessive amount of gas.
So we built upon the existing open-source solutions that used Merkle proofs for airdrops — namely, Uniswap's implementation — and changed it to allow for a percentage allocation instead of an exact amount. We also added some more functionality to allow for continual use of the contract. One can fund the contract multiple times and allow recipients to keep claiming their allocation. We have open-sourced Splits here: https://github.com/mirror-xyz/splits
The challenge with allowing continual claiming of an allocation is that, once someone has claimed their allocation, we need some way to prevent them from claiming the same quantity again. A recipient should only be able to claim once from a given deposit.
One way to solve this problem would be to track each claim, but this would use a lot of storage and gas to operate. Instead, we decided to add a concept of a claim "window" — an interval for which you could claim an allocation, at any time. The flow for this model is as follows:
- The allocation is specified, and the Merkle proof is set on the contract
- Funds are deposited into the contract
- The window is incremented from 0 to 1
- Anyone can claim their allocation for the first window
- More funds are added
- The window is incremented from 1 to 2
- Anyone can claim their allocation for the first or the second window, or both in the same transaction
In this way, we allowed perpetual claiming of allocations from a contract using Merkle proofs!
We think it would be interesting if someone could claim their funds across multiple splits from one address. This would allow someone to accrue revenue continuously from being involved in different splits, and then be able to withdraw all of that in one transaction.
It's also unclear at the moment how this is implemented on layer 2. It might be the case that L2 transfers are so cheap that much of this is over-engineering for a given use-case. We'll have to do more experiments to see, but at the moment there is some demand for this functionality directly on L1.
It would also be interesting to see this architecture being used for important projects such as governance funding — whereby a percentage allocation is budgeted for different departments, and now can be fully transparent and on-chain.
We've also received feedback from people that they'd like to see a real-time dashboard of all the created splits and how much people have received over time. Eventually it could become one component of generating on-chain reputation for applications like credit scores.
It is important to note that splits currently only work with ETH. ERC-20 tokens sent to a split will be permanently lost.