Generalized property tests for ERC4626 vaults

Daejun Park

As DeFi grows and matures, scalable infrastructure and composability are top of mind for developers. Ethereum Requests for Comments (or ERCs) — standardized toolkits for building Ethereum-based apps, such as the widely used token standard ERC20 — serve the essential role of providing consistent guidelines for developers to contribute to the ecosystem without starting from scratch. Earlier this year, tokenized vault standard ERC4626 was created to encourage cross-compatibility among yield-bearing tokens. Standardizing implementations details can also address pressing composability issues, making protocol integration easier and ultimately less error-prone.

Several DeFi projects have already adopted the standard, looking to increase the composability of their vaults, and we anticipate wider adoption throughout the ecosystem. Adapting existing vaults does cause some growing pains, however; critically, certain implementation errors can expose new targets for attack. Even small errors (as small as misinterpreting the standard interface) can have significant consequences for both security and user experience, underscoring the need for more security tools and measures, particularly within a more composable DeFi ecosystem. 

Fortunately, simple errors can have relatively simple solutions if detected well before they’re exploited (and ideally before they’re even deployed). To that end, we released ERC4626 property tests for fuzzing and symbolic execution to help vault builders detect standard violations that may break integrations or lead to vulnerabilities down the line. In this post, we explain the motivating problem, walk through our approach, and conclude with some actionable advice.

First a little background on the ERC4626 standard

Finalized in March, ERC4626 is the standard for tokenized vaults. It was introduced in order to extend the widely used ERC20 standard (currently the base for hundreds of tokens), encourage standardization across yield-bearing vaults, and ensure composability for the apps and protocols (e.g. yield aggregators) that need to interact with them. This means that any app built on an ERC4626 vault can be easily extended to work with any other ERC4626 vault.

Tokenized vaults allow users to freely deposit assets to mint vault shares, and later redeem those shares to withdraw principal and interest from the vault. These vault shares are ERC20 tokens, and thus can be easily traded or used as collateral to borrow other assets. For example, users can deposit their assets in Yearn vaults to mint yVault tokens, which can then be traded on Uniswap, staked for additional yield, or used as collateral for lending protocols.

The business logic for generating and distributing yield (and determining the share price) may vary across implementations. In order to cover as many vaults as possible (with the goal of making them interoperable vs. identical), the ERC4626 standard focuses on describing the user interface, leaving most of the implementation details unspecified. This allows for variations in business logic as long as the vault meets the specific requirements of the interface, and encourages interoperability across many different kinds of apps and types of ERC4626 vaults.

As more vaults are created, we expect to see them implemented according to the ERC4626 standard from the start; but we’re currently in a somewhat transitional phase, where developers looking to take advantage of greater composability will need to update existing vaults, apps, and protocols to conform with the standard. And as they upgrade, they contend with a number of complexities and challenges. 

The challenges of standard conformance (and the pitfalls of failing to conform)

Following a new standard isn’t always straightforward. Every ERC4626 vault must faithfully (and exactly) implement the standard’s requirements as described. Otherwise, the integration of ERC4626 vaults becomes increasingly complex to account for different variations. This complexity makes integrations inherently error-prone; and because they aren’t sufficiently future proof, they may lead to security vulnerabilities over time.

Non-standard ERC20 tokens (e.g., Tether USD) require many DeFi systems to use an additional library (such as SafeERC20) when performing token transfers to safely deal with diverging behaviors (for example returning nothing when a transfer succeeds instead of returning true). This means that any systems interacting with these tokens could become vulnerable if the system isn’t designed to properly handle cases of “missing returns.” These scenarios can potentially introduce a common security pitfall, and increase overall development and maintenance costs (when factoring in the additional logic and dependencies needed to mitigate issues). Conforming to the standard is therefore critical not only for individual implementations but also for the security of the entire ecosystem. One vulnerability in a single system or dependency can cause widespread issues.

Ideally, standards would be formally specified without ambiguity (e.g., formal specification of ERC20), and every implementation could be formally verified against the standard specification. In practice, however, this isn’t easy to achieve in a short period of time, due the cost and effort required from the community.

Introducing executable ERC4626 properties to identify conformance issues

As we work toward an ideal state (every vault formally verified against rigorous formal specifications), we’ve written ERC4626 standard properties to catch discrepancies in subtle, easy-to-miss details of the standard requirements.  

Vault developers can run the tests to detect potential standard violations in their implementations before deployment. And vault integrators can check if the given vaults follow the standard before integrating them into their system. The properties can also be tested against the live vaults already deployed on the mainnet, via mainnet fork testing. Testing live vaults may be useful — especially when the vaults have been deployed or upgraded recently — to ensure that all the system parameters have been set correctly. 

We chose property-based tests — written in Foundry and ready to be run by its fuzzer — in order to make the properties executable (and thus testable). In the future, they may also be run by symbolic execution or model checking tools to formally verify that the given vault satisfies the properties for all possible inputs and conditions.

We wrote the properties to be general enough to be applied to a wide range of vaults that implement different business logic. We used only public interface functions to make them agnostic to implementation details. (Due to this restriction, however, certain standard requirements that refer to implementation-specific internal data were omitted from the properties.)

For example, the following property corresponds to one of the requirements of the convertToShares() function, “MUST NOT show any variations depending on the caller.” Given the two account addresses and the amount, it makes each of the accounts call convertToShares() with the same amount, and ensures the two return values are equal. This property is independent of the implementation details of convertToShares(), which varies across vaults and must be satisfied by any vaults that implement ERC4626. This property can be executed by providing specific input values (for unit testing), many random inputs (for fuzz testing), or symbolic values (for symbolic execution and formal verification). It can also be run locally or against a mainnet fork (for integration testing).

snippet from ERC4626 property tests

Use case: properties that test for rounding errors

Rounding errors, for example, are an important class of (seemingly minor) bugs that can have some series implications. The underlying accounting logic of ERC4626, e.g., calculating the number of shares to be minted, or the amount of assets to be withdrawn, is implemented using fixed-point arithmetic — rounding errors are inevitable. For security, however, the standard explicitly specifies the preferred rounding direction for each interface function, while leaving the error bounds unspecified and implementation-dependent. Specifically, the deposit() and redeem() functions should return an under-approximation of the exact value, while the mint() and withdraw() functions should return an over-approximation. For example, if the current share price (i.e., the amount of assets per share) is 2, then deposit() with 3 wei of assets should only mint up to 1 wei of shares (i.e., floor(3/2)), while withdraw() with 3 wei of assets should burn at least 2 wei of shares (i.e., ceil(3/2)).

We wrote the rounding-related properties to be independent of the underlying accounting logic by treating it as a black box. Specifically, we formulated them as so-called “round-trip” properties, which describe the relationship between two opposite functions. For example, the following property specifies that withdrawing assets that have just been escrowed by minting N shares must burn no less than N shares. In other words, no one can make a free profit converting assets and vault shares back and forth by repeatedly minting and withdrawing.

snippet from ERC4626 property tests

Indeed, we found that several ERC4626 vaults on mainnet fail to satisfy the above property due to rounding errors. This means that anyone could earn, for example, a couple of satoshi BTC (1 satoshi ~= 0.02 cent at the time of writing) by simply (and repeatedly) minting and withdrawing, slowly draining the vault. This may actually turn a profit on chains that enjoy very low gas fees (e.g., Fantom), or if the asset price becomes high enough in the future.

Testing the ERC4626 standard in the wild

We tested our properties against ~100 ERC4626 vaults on mainnet, and found many vaults that failed to follow the standard requirements — mostly due to the rounding errors (e.g., using floor rounding where ceiling is desired, as we described). Specifically, certain vaults failed to mint the exact number of shares requested by the mint() function, although the standard explicitly requires this. Some of them also emitted an inconsistent Deposit event where the logged data is different from what was actually minted. To our surprise, some vaults never minted on the spot at all; instead, they just put the mint requests into a queue, and process them later in a batch as a separate transaction.

Although these divergent behaviors were not exploitable per se, they may become vulnerable when integrated into other systems that expect only the standard behaviors. These issues will make vault integration much harder, potentially neutralizing the ongoing efforts and driving motivation behind standardization.

Using our property tests, and other actionable steps toward standard conformance

Following the standard precisely can prevent divergent behaviors (ideally before they’re ever deployed). We hope our properties help, along with a few additional action items. For those who are developing and/or integrating ERC4626 vaults:

  • We highly recommend running our property tests against your vaults. They will quickly find issues if there are any clear violations of the standard.
  • We also suggest reviewing our properties to cross check your understanding of the standard requirements, and adjust your implementation if there is any unintentional discrepancy.
  • If your vault has to diverge from the standard, we recommend clearly documenting the non-standard behaviors, so that others can properly handle these deviations when integrating with your vault. Please note that this should be considered as a last resort.

***
ERC4626 vaults have the potential to become an important building block for DeFi in the foreseeable future — and, for the sake of composability, it’s important for both new and existing vaults to follow the standard. New implementations will emerge following the standard, so there’s no better time than the present to standardize existing vaults.

As we work toward an ideal state (where different vaults are uniformly composable), ERC4626 property tests can be run to more easily detect standard violations in vault implementations. The property tests (with documentation and examples) are all publicly available in our Github repository. We welcome your feedback and contributions!

***
The views expressed here are those of the individual AH Capital Management, L.L.C. (“a16z”) personnel quoted and are not the views of a16z or its affiliates. Certain information contained in here has been obtained from third-party sources, including from portfolio companies of funds managed by a16z. While taken from sources believed to be reliable, a16z has not independently verified such information and makes no representations about the current or enduring accuracy of the information or its appropriateness for a given situation. In addition, this content may include third-party advertisements; a16z has not reviewed such advertisements and does not endorse any advertising content contained therein.

This content is provided for informational purposes only, and should not be relied upon as legal, business, investment, or tax advice. You should consult your own advisers as to those matters. References to any securities or digital assets are for illustrative purposes only, and do not constitute an investment recommendation or offer to provide investment advisory services. Furthermore, this content is not directed at nor intended for use by any investors or prospective investors, and may not under any circumstances be relied upon when making a decision to invest in any fund managed by a16z. (An offering to invest in an a16z fund will be made only by the private placement memorandum, subscription agreement, and other relevant documentation of any such fund and should be read in their entirety.) Any investments or portfolio companies mentioned, referred to, or described are not representative of all investments in vehicles managed by a16z, and there can be no assurance that the investments will be profitable or that other investments made in the future will have similar characteristics or results. A list of investments made by funds managed by Andreessen Horowitz (excluding investments for which the issuer has not provided permission for a16z to disclose publicly as well as unannounced investments in publicly traded digital assets) is available at https://a16z.com/investments/.

Charts and graphs provided within are for informational purposes solely and should not be relied upon when making any investment decision. Past performance is not indicative of future results. The content speaks only as of the date indicated. Any projections, estimates, forecasts, targets, prospects, and/or opinions expressed in these materials are subject to change without notice and may differ or be contrary to opinions expressed by others. Please see https://a16z.com/disclosures for additional important information