Blueprints – DeFi Composability Endgame
A long time ago, I thought of building an exchange. I didn't want it to accept "just" ERC20 tokens or any other single token type. I didn't care what kinds of tokens were traded, I wanted to be able to trade all of them. Why can't we abstract tokens' type away? Other times I wondered about derivatives and "money legos" and how tokens can be composed in a standardized way. Blueprints is the solution I have been thinking about for quite a while.
If you want to go straight to the beef without picking your mind about the context, jump straight here. You can always read the motivation later.
Composability.
Uniswap revolutionized finance forever. Yeah, swapping trustlessly is cool, but what's even cooler is that we can trade any asset we choose. Not just 500 tokens some exchange listed, any token. I can create a token, and without asking anyone for permission, we can trade it.
Composability is the ability to assemble components to create custom structures. In finance, the main operand is a financial position, and yet we aren't mature enough to have made it composable.
There could be other types of entities we'd like to compose in financial applications, but we'll focus on financial positions as a building block – others likely only require a standard interface to plug into (to create a financial position).
Positions.
The primary operand in finance is a financial position. If I buy ETH, I'm entering a position. When I short it, that's another position. I'm taking a position by taking a loan from my favorite bank. When buying insurance, I'm (yet again) entering a position.
Any financial operation is changing something about our financial position – we use verbs like "enter", "exit", "change", and "take" when describing them. But fundamentally, positions are just the things we own, and we may change some things about them so that we own something different. Changing positions is no different from trading (when I extend my loan, it's as if I sold my previous debt position to the bank, which annihilated with the bank's position, and bought another position from the bank with a longer repayment plan).
DeFi protocols express positions in many different ways. Some examples:
Uniswap v1-v2: have an ERC20 token that expresses the deposit associated with any pair
Uniswap v3-v41: have owner-associated positions that express the deposit; periphery codebase allows for tokenizing those positions into NFT (ERC721) tokens; v4 also has an optional ERC6909 token accounting of deposited ERC20 tokens
Polymarket: given an ERC20 token, produces a set of ERC1155 tokens that express payouts conditional to selected outcomes2
friend.tech: has an owner-associated balance; not even ERC20-compatible, wrappers needed to be created to transfer tokens
Aave: can lend and borrow ERC20 tokens, there is an owner-associated borrow position, while vault shares are rebasing ERC20 tokens
As we can see, the positions are expressed very differently – sometimes as an owner-position registry, sometimes ERC20 or rebasing ERC20 (I separated them because integrating with rebasing tokens requires quite different systems), other times ERC721, ERC1155, or even ERC6909.
Building.
You want to build a truly composable financial primitive. So, you want it to be compatible with all the tokens out there, right? Well, ERC20 tokens are very popular for financial primitives, you also want users to be able to use ERC721 and ERC1155. Maybe you don't even care about rebasing tokens. If you take this seriously, you end up with a 900-lines of code file and 200 more lines to manage it. That's how far we've evolved!3
So, maybe we shouldn't integrate all the tokens? Which token standard(s) should we use (and integrate with) if we're creating derivatives (lending, perps, options, insurance, stablecoins – anything that is based on other tokens)?
Even novel codebases like Uniswap v4 or Euler v2 chose to accept only ERC20 tokens. This means that despite our desire to create more custom positions to make financial positions better fitting for owners, we either need to express them in an extremely suboptimal way (using ERC20 for every single token type) or be doomed not to be compatible with mainstream "revolutionary" and "composable" products.4
Problems.
So, what problems are yet to be solved and what should an ideal system look like?
The problems:
there is no unified way to interact with any token
one needs to consider adversarial models since the implementation of tokens is arbitrary:
arbitrary gas consumption
potential ability to reenter
potential to arbitrarily change the behavior (apply
balanceOf
decrease without transfers out, for example)potential to revert for arbitrary reasons
moreover, transferring many tokens requires interacting with many contracts, which requires accessing a lot of state (which is and will remain expensive for blockchains)
The first problem could be solved with a library that can interact with many types of tokens, and standardize the interactions with them to a few functions, similarly to how SafeERC20
works, but for multiple token types. Unfortunately, this would:
not fully solve the second main problem, or partially solve some of the points at a cost (like introducing reentrancy protection; but that would limit the usability – some designs desire reentrancy),
bloat all contracts interacting with tokens,
not work for tokens outside of the most popular standards,
cause security issues that require additional state accesses/function calls to mitigate (for example, ERC20 and ERC721 have the same
transferFrom
signature, so ERC721 tokens could be withdrawn by users pretending them to be ERC20 tokens).
Using a token wrapper solves all of these problems. But it also needs to be expandable to be able to wrap all kinds of tokens.
Blueprints.
My proposal to solve these problems is the Blueprints ecosystem. It is composed of the BlueprintManager and Blueprints.
BlueprintManager is an ERC6909 contract (can hold multiple tokens and avoids ERC1155 bloat) that is planned to have a deployment on every EVM network. Blueprints are smart contracts that integrate with it. They manage the creation of tokens. Anyone can create and use Blueprints. They allow for the creation of custom tokens in ways described by their logic – they describe the way created tokens behave; they give them meaning. Blueprints define the "construction" of the token. Blueprints range from simple contracts that just wrap another token standard (e.g. ERC20Wrapper<USDC>
), to complex ones that describe tokens with custom logic.
Using Blueprints, we can describe any token. For example, we can describe a European call option in our composable universe by simply writing: EuropeanCallOption<ETH, USDC, 3000, 1 Jan 2025; 12:00 UTC>
. These mean the speculative asset (ETH), base asset (USDC), strike price of $3k, and the date of expiry of UTC noon on Jan 1, 2025, respectively. EuropeanCallOption
is defined by a Blueprint, which allows owners to exercise it (per option mechanics).
We can use this token further as an argument to construct more sophisticated tokens, for example, I may want to bundle it with another option on the same pair and with the same expiry, but with a $2k strike price. I get:
Basket<[EuropeanCallOption<ETH, USDC, 3000, 1 Jan 2025; 12:00 UTC>, EuropeanCallOption<ETH, USDC, 2000, 1 Jan 2025; 12:00 UTC>], [1, 1]>
, where the first array specified which tokens do I want to group, and the second one defined their proportions. This is a basket of options, and I can speculate on volatility by buying/selling it. Owning it is not too interesting – it's no different than owning the underlying tokens, but it gets interesting when we compose it further: we can create a $1000 call option on this basket of options, expiring 1 month before the underlying options!
EuropeanCallOption<Basket<[EuropeanCallOption<ETH, USDC, 3000, 1 Jan 2025; 12:00 UTC>, EuropeanCallOption<ETH, USDC, 2000, 1 Jan 2025; 12:00 UTC>], [1, 1]>, USDC, 1000, 1 Dec 2024; 12:00 UTC>
, done!
Now, we can create any token using a few basic primitives. Any of us can add primitives if we're missing one. This is the composability endgame.
Blueprints – technicals.
Nearly all usage of Blueprints should be done through the BlueprintManager. Blueprints have one most significant function for BlueprintManager to invoke: executeAction(bytes)
. Blueprints are free to interpret the argument however they want, they just have to return 4 arrays of (tokenId, amount)
pairs:
mint
– the tokens BlueprintManager is supposed to mint; note that the BlueprintManager will hash this value with Blueprint's address to derive the minted token's id – the id passed is internal to the Blueprint,burn
– the tokens BlueprintManager is supposed to burn; again, the ids are internal to the Blueprint,give
– the tokens external to the Blueprint (possibly minted by other Blueprints) that should be given (transferred) to the sender,take
– the tokens external to the Blueprint that should be taken (transferred) from the sender to the Blueprint.
With these actions, Blueprints can take and give back collateral, as well as mint and redeem (burn) their own tokens. Users can use a checksum to make sure that Blueprints don't misbehave. Hence, and because the token transfers and balances are managed solely by BlueprintManager, users don't have to trust Blueprints to know that the tokens are working predictably. It's also possible to invoke actions for other users, but the tokens charged from burn
and take
actions are accounted in line with approval logic – the caller needs to either be an operator for the user or have sufficient allowance that will be decreased.
Additionally, Blueprints have mint()
functions available in the BlueprintManager, since it's sometimes useful for that function to be called directly, in case the receiver doesn't need to be charged – this is especially useful for tokens with gated access to minting.
Blueprints – possibilities.
With the above construction of BlueprintManager, one can integrate any token-agnostic infrastructure with it, and forget about the existence of more than one token type.
A few things that can be implemented with Blueprints:
token wrappers: a wrap action can transfer tokens from the user to the Blueprint, and use
mint
to create respective tokens; unwrapping transfers the tokens from the Blueprint to the user and uses theburn
array,derivatives: these will likely use all four return arrays across all actions – for example, creating options will require
mint
andtake
(for collateral), while redeeming will useburn
andgive
– it burns the option token, and gives the appropriate value back,native BlueprintManager tokens: tokens can be created natively in the BlueprintManager, only an invocation of one of the mint functions is needed; one can also design own minting schedules, etc., without the need to implement the token itself; the token will be natively compatible with every integration, and will require no additional gas costs to turn it into another native form if required.
Moreover, systems that use Blueprints instead of their own ERC1155, ERC6909, or even a multi-ERC20 token approach can always use a wrapper to wrap BlueprintManager into an ERC1155 token, ERC20 tokens, or any other kind that is useful for its integrators.
Blueprints – implementation.
A draft of the implementation of the BlueprintManager will be shared shortly.
After that, everyone will be invited to discuss the implementation and design details of the BlueprintManager. All discussions will be public, and everyone will have the opportunity to be considered a contributor to the BlueprintManager codebase.
Further, Blueprints' behavior guidelines will be published. These best practices are designed to improve the security and usability of a Blueprint, not to enforce the security of the whole system.
If you have an idea for a Blueprint, please share it! If you're building a DeFi product and are unsure whether it can be integrated with Blueprints, please reach out! We can help scope (and design) a minimal set of Blueprints that will allow your product to be implemented, and there is a great chance that we can unify liquidity for your product with existing products, or find market makers interested in taking some positions in the protocol. This also means that you can save on smart contract development and auditing costs, and ensure the composability of your product with others!
Uniswap v4 architecture is possibly not final. ↩
Actually, these conditional tokens were developed by Gnosis; Polymarket uses them. ↩
Yeah, many lines of code are comments, but still… Quite a lot of (work and) code that introduces risks – verifying the correctness of the contract is not trivial. ↩
I am not criticizing Uniswap's or Euler's design choices, only noticing that something is lacking in new codebases to achieve composability. ↩