What a sad day for the entire crypto ecosystem -- FTX the second-largest exchange blew up unceremoniously. Billions of dollars in retail money, investments, and crypto industry progress were vaporized overnight.
In just the last year, we’ve seen multiple centralized custodial entities like 3AC, Luna, Celsius, etc gamble with user funds and cause retail to be utterly wiped out. The narrative that centralized regulated entities protect users has been completely invalidated at this point.
We as builders in the industry need to push for the original ethos of crypto: non-custodial, permissionless, and open systems.
DeFi has made great inroads after each significant previous failure and will continue to become anti-fragile. Uniswap was built during the 2018 bear market and now has trading volume comparable with Coinbase.
However, most volume is still on centralized exchanges today. This is because there are still problems to solve in DeFi like slippage, gas fees, MEV, privacy, advanced order types, etc
.
With V1 of our product, we are solving the first 3 issues: slippage, gas fees, and MEV, to incentivize large-volume trades on-chain instead of going to OTC desks.
The flip side of the problem is attracting and rewarding LPs for the liquidity needed to complete these trades. Although DeFi opened up the opportunity for anybody to be a market maker, unresolved MEV and toxic order flow issues remain with current DEX models.
In our August update, we presented our solution to MEV extraction and the organic ability TWAMMs have to easily discriminate toxic from non-toxic order flow. This enables TWAMM to properly charge appropriate fees for toxic orders, automatically re-distributing them back to LPs.
In the September update, we showed what is possible when you build a venue for large non-toxic trades, incentivize arbitrage, and reward LPs for their liquidity.
Sacrificing user safety for convenience is no longer an acceptable tradeoff. We can build better financial systems instead of inheriting past mistakes. Future versions of our product will solve the issues of privacy and advanced order types
to enable parity with current systems without compromising the ethos of DeFi.
Optimize, optimize, and then optimize some more. Optimization was the bulk of our work last month in conjunction with numerical analysis and quantifying of operational parameters. Portions of the optimization process, techniques, and results are presented below.
Optimization design changes are run against a battery of tests to ensure that operational behavior remains within minimum tolerances and both contract size and gas usage are improving or degradations are understood. Occasionally tradeoffs must be made and changes to facilitate numerical safety or reduce contract size in exchange for marginal increases in gas use are accepted.
We formally documented 38 change iterations, 9 of which were reverted, since our last update—a figure showing the averaged gas use of the minimum, maximum, and average measured operations in a benchmark design (neglecting transfers and approvals) appears below:
Across all three measured averages, the design process resulted in a reduction of nearly 20,000 gas (!!), while concurrently introducing checks for safe arithmetic operations, including shifting and overflow and underflow of non-standard word sizes. This result was achieved while reducing the contract size to a deployable 23.825k. The figure below shows the evolution of the contract size through the design process (figures for the first four iterations are not available at this time):
As part of the aforementioned tradeoffs, assembly language in the form of Yul, was declared off-limits because we decided to trade off additional gas use and contract size reductions for reduced development time and complexity.
Efficient Errors
However, one major exception to this declaration is the adoption of assembly code to efficiently revert with error codes, considerably reducing the size of our TWAMM contract. The original source code that was adapted for our purpose can be found in Balancer’s core contracts[1].
Conventions
Another forthcoming objective is to get reviews from both formal auditors and the broader DeFi community. To that end we integrated Lasse Herskind’s Solhint configuration and rules [2] into our process, modifying our code to conform to conventions and addressing any outstanding best practice violations.
The greatest gains are often obtained with high-level optimizations, for instance using the FRAX mathematical approximation to compute reserve updates when concurrent opposing trades are active. The effect and benefits of this optimization are discussed in [3]. As our design has evolved, the number of high-level optimizations to exploit has greatly diminished. The last remaining high-level optimizations were arrived at by examining our design using the Solidity Inspector Surya [4].
High-Level Optimizations
Surya provides a graph of the contract’s functions and their interactions. Examining this graph allowed us to identify duplicate function calls and code paths, which could then be unified to reduce contract size. For example, in the subsection of graph in Figure 4, Surya reveals that there are 5 discrete callers of the private function _executeVirtualOrders
.
Low-Level Optimizations
The greatest gains in Ethereum smart contracts with respect to gas use are available by designing systems that interact with the least number of 256-bit storage slots in every transaction. It is well-known that re-sizing variables and arranging their declarations permit Solidity to pack them efficiently and reduce gas use. For example, consider the following four state variables:
uint8 public a;
uint256 public b;
uint128 private c;
uint16 private d;
Only two slots are required to store the 408-bits in all the variables declared above, but because of the way they are ordered, the compiler will likely use 3 256-bit slots, generating a gas inefficient design.
Furthermore, the compiler generates additional code that increases contract size and gas used for any variables that are not at that default machine word length of 256-bits. Finally, notice that the variables above are all multiples of eight—what if smaller sizes that are not multiples of eight are all that are required?
These considerations were all explored in the design change iterations for which measurements are shown in Figures 2 and 3. Ultimately a systematic approach to optimizing gas use by minimizing slot accesses per transaction through aggressive bit-packing was used to achieve improved results.
Systemic Gas Use Reductions With Bit-Packing
The first step in the system is to list out all state variables used in the design with their Solidity native data types, absolute minimum and maximum values, and then their absolute number of required bits for representation.
Once this step is done, it’s possible to then arrange the state variables in such a way that their absolute number of bits sums as closely as possible to the slot maximum of 256-bits.
In some situations, state variables’ maximum amounts can be reduced to save on the number of bits of required storage, trading off some contract functionality as a result. This is evident in Table 1, below, for both the Balancer Fees and Cron-Fi Fees, which have been reduced to 96-bits. Since these fees are collected on join or exit events or via a withdraw call, respectively, it’s possible to intervene and collect them should they approach their maximum. The contract has been written to clamp these values to their maximum 96-bit amount in the case of overflow. Additional amounts are distributed back to the pool’s reserves.
Many of the values shown in Table 1 were constrained needlessly to Solidity’s native types which are multiples of 8-bits. A considerable amount of storage can be saved by storing them according to their absolutely required ranges, for instance, all of the fee variables, shortTermFeeBP, parnterFeeBP, and longTermFeeBP, need only 10-bits.
All of the variables shown in Table 1 are stored in 256-bit Solidity variables and accessed via a bit-packing library. An example of methods in the library to access and store feeShiftU3 is shown in the code below. Note that the offset variable shown below corresponds to the offset shown in Table 1 and is closely related to the Least Significant Bit (LSB) of feeShiftU3 in the slot.
The pack and unpack methods above convert a 3-bit value stored inside of a 256-bit slot with 4 other values into a 256-bit variable that can be manipulated, then allowing it to be re-packaged with the other variables when changed. Note that despite the variable being 3-bits, it is returned by the function unpackFeeShiftS3 as a uint256. This reduces the contract size and gas used by the contract but requires discipline on the part of the developer to utilize and manipulate the value appropriately (for instance it’s possible to add a value to the returned variable, overflow it, and cause a logic error in downstream code).
Usage Patterns
Further gas use reductions can be had by understanding which state variables are accessed by the different functions in a contract. By aggregating commonly used state variables together into the same slot, gas use can be reduced. The arrangement of Table 1 belies this optimization, specifically slot 3. Slot 3 contains all of the variables required to compute Cron-Fi fees, which are disabled by default. The state variables for the holding period and penalty are not used in swaps, so they are conveniently stored in slot 3 along with the Cron-Fi fees.
To compute the pool’s reserve though, it is necessary to subtract the orders, proceeds, and any fees from the Balancer Vault’s accounting of the tokens in the pool. This means that the Cron-Fi fees must be used to compute the pool reserves—a computation used in nearly any function. However, since these fees are disabled by default, a flag called zeroCronFiFees is used to determine if the values in slot 3 need to be read—if this flag is set to one, then the Cron-Fi fees are zero and need not be used to compute the pool’s reserves. A conceptual diagram of how zeroCronFiFees eliminates the need to read from slot 3 to compute reserves for token 0 is shown in Figure 5.
A similar pattern is used to reduce gas in the contract by preventing the reading of the fee address; the boolean variable feeEnabled
in slot 4 indicates if this address is set and does not need to be read during contract transactions.
A host of other optimizations is beyond the scope of this update, but include further optimizations related to usage patterns to reduce function call overhead, etc. based on operations unique to the pool.
BalancerErrors.sol (2021). Online. Available: github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol. Accessed: Oct. 2022.
Solhint. Online. Available: github.com/LHerskind/solhint. Accessed: Oct. 2022.
“TWAMM Algorithm Optimization & Approximation Analysis“, April 2022. Online. Available: mirror.xyz/0slippage.eth/5zKJW4Zx9zYHpB4jNln16HuU8d8EtawmA17usNfIje4. Accessed: Nov. 2022.
SĹ«rya, The Sun God: A Solidity Inspector. Online. Available: github.com/ConsenSys/surya. Accessed: Oct. 2022.