A New Model of DeFi Contract Security: Focus on Protocol Invariance

Summary

Don't just write require statements for specific functions; write require statements for your protocols. Function compliance checks (requirements)-effectiveness (Effects)-interactions (INteractions) + protocol invariance (Iniants) or FREI-PI mode can help your contract be more secure, because it forces developers to focus on function-level security, Also watch out for invariants at the protocol level.

motivation

In March 2023, Euler Finance was hacked and lost $200 million. Euler Finance is a lending marketplace where users can deposit collateral and borrow against it. It has some unique features, in fact they are a lending marketplace comparable to Compound Finance and Aave.

You can read a postmortem about this hack here. Its main content is the lack of health checks in a specific function, allowing users to break the fundamental invariance of the lending market.

Fundamental Iniants

At the core of most DeFi protocols is an immutability, a property of program state that is expected to always be true. It is also possible to have multiple invariants, but in general, they are built around a core idea. Here are some examples:

  • As in the lending market: users cannot take any action to place any account in an unsafe or less secure collateral position ("less safe" means it is already below the minimum safety threshold and therefore cannot be withdrawn further).
  • In AMM DEX: x * y == k, x + y == k, etc.
  • In liquidity mining staking: users should only be able to withdraw the amount of staking tokens they deposited.

What went wrong with Euler Finance wasn't necessarily that they added features, didn't write tests, or didn't follow traditional best practices. They audited the upgrade and had tests, but it still slipped through the cracks. The core problem is that they forget about a core invariant of the lending market (as do auditors!).

*Note: I'm not trying to pick on Euler, they are a talented team, but this is a recent case. *

The core of the problem

You're probably thinking "Well, that's right. That's why they got hacked; they forgot a require statement". Yes and no.

But why would they forget the require statement?

check - validate - interaction is not good enough

A common pattern recommended for solidity developers is the Checks-Effects-Interactions pattern. It's very useful for eliminating reentrancy-related bugs, and often increases the amount of input validation a developer has to do. _But_, it is prone to the problem of not seeing the forest for the trees.

What it teaches the developer is: "First I write my require statement, then I do validation, then maybe I do any interaction, then I'm safe". The problem is, more often than not, it becomes a mixture of checks and effects -- not bad, huh? Interactions are still final, so reentrancy is not an issue. But it forces users to focus on more specific functions and individual state transitions rather than the global, broader context. This means that:

The mere check-validate-interaction pattern makes developers forget about the core invariants of their protocols.

It's still an excellent pattern for developers, but protocol invariance should always be ensured (seriously, you should still be using CEI!).

Do it right: FREI-PI mode

Take for example this snippet from dYdX's SoloMargin contract (source code), which is a lending market and leveraged trading contract. This is a good example of what I call the Function Requirements-Effects-Interactions + Protocol Iniants pattern, or the FREI-PI pattern.

As such, I believe this is the only lending market in the early-stage lending market that does not have any market-related vulnerabilities. Compound and Aave have no direct issues, but their forks have. And bZx has been hacked many times.

Examine the code below and notice the following abstractions:

  1. Check input parameters (_verifyInputs).
  2. Action (data conversion, state manipulation)
  3. Check the final state (_verifyFinalState).

The usual Checks-Effects-Interactions are still performed. It is worth noting that check-validation-interaction with additional checks is not equivalent to FREI-PI--they are similar but serve fundamentally different goals. Therefore, developers should think of them as different: FREI-PI, as a higher abstraction, aims at protocol safety, while CEI aims at functional safety.

The structure of this contract is really interesting - users can perform the actions they want (deposit, borrow, trade, transfer, liquidate, etc.) in a chain of actions. Want to deposit 3 different tokens, withdraw a 4th, and liquidate an account? It's a single call.

This is the power of FREI-PI: users can do whatever they want within the protocol, as long as the invariants of the core lending market hold at the end of the call: a single user cannot take any action that would place any account insecure or more Insecure collateral positions. For this contract, this is performed in _verifyFinalState , checking the collateral of each affected account to ensure that the agreement is better than when the transaction was started.

There are some additional invariants included in this function that complement the core invariants and help with side functions like closing markets, but it's the core checks that really keep the protocol secure.

Entity-centric FREI-PI

Another problem with FREI-PI is the entity-centric concept. Take a lending market and assumed core invariants as an example:

Technically, this isn't the only invariant, but it is for the user entity (it's still a core protocol invariant, and usually user invariants are core protocol invariants). Lending markets also typically have 2 additional entities:

  • Oracle
  • Management / Governance

Every additional immutability makes the protocol more difficult to guarantee, so the less the better. This is actually what Dan Elitzer said in his article titled: Why DeFi is Broken and How to Fix It #1 Oracle-less Protocol (hint: the article doesn't actually say that oracles are the problem).

Oracle

For oracles, take the $130 million Cream Finance exploit. The core immutability of oracle entities:

As it turns out, verifying oracles at runtime with FREI-PI is tricky, but doable, with some forethought. Generally speaking, Chainlink is a good choice to rely on mostly, satisfying most of the immutability. In the rare case of manipulation or surprise, it may be beneficial to have safeguards that reduce flexibility in favor of accuracy (such as checking that the last known value was a few percent greater than the current value). Also, dYdX's SoloMargin system does a great job with their DAI oracle, here's the code (if you can't tell, I think it's the best complex smart contract system ever written).

For more on oracle evaluation, and highlighting the capabilities of Euler's team, they wrote a good article on computationally manipulating Uniswap V3 TWAP oracle prices.

Administration / Governance

Creating invariants for managed entities is the trickiest. This is mainly due to the fact that most of their role is to change existing other invariants. That said, if you can avoid using administrative roles, you should.

Fundamentally, the core invariants of a managed entity might be:

Interpretation: Admins can do things that should end up not breaking invariants, unless they change things drastically to protect users' funds (eg: moving assets into a rescue contract is removal of invariants). Admins should also be considered a user, so the user invariance of the core lending market should also hold for them (meaning they cannot attack other users or protocols). Currently, some administrator actions are impossible to verify at runtime via FREI-PI, but with sufficiently strong invariants elsewhere, hopefully most of the problems can be mitigated. I say currently, because one can imagine using a zk proof system that might check the entire state of the contract (per user, per oracle, etc.).

As an example of an administrator breaking immutability, take the Compound governance action that borked the cETH market in August 2022. Fundamentally, this upgrade breaks Oracle's immutability: Oracle provides accurate and (relatively) real-time information. Due to missing functionality, Oracle can provide incorrect information. A run-time FREI-PI verification, checking that the affected Oracle can provide real-time information, can prevent this from happening with the upgrade. This can be incorporated into _setPriceOracle to check if all assets received the realtime information. The nice thing about FREI-PI for admin roles is that admin roles are relatively insensitive to price (or at least they should be), so more gas usage shouldn't be a big problem.

Complexity is Dangerous

So while the most important invariants are the core invariants of the protocol, there can also be some entity-centric invariants that must be held by the core invariants. However, the simplest (and smallest) set of invariants is probably the safest. Simple is good A shining example is Uniswap…

Why Uniswap has never been hacked (probably)

AMMs can have the simplest basic invariance of any DeFi primitive: tokenBalanceX * tokenBalanceY == k (e.g. constant product model). Every function in Uniswap V2 revolves around this simple invariant:

  1. Mint: added to k
  2. Burn: subtract from k
  3. Swap: transfer x and y, but keep k.
  4. Skim: readjust tokenBalanceX * tokenBalanceY to make it equal to k, and remove the redundant part.

The security secret of Uniswap V2: the core is a simple immutability, and all functions are in service of it. The only other entity that can be argued is governance, which can turn on a fee switch, which doesn't touch the core immutability, just the distribution of token balance ownership. This simplicity in their security statement is why Uniswap has never been hacked. Simplicity is not actually a contempt for excellent developers of Uniswap's smart contracts. On the contrary, it takes excellent engineers to find simplicity.

Gas problem

My Twitter is already full of optimizationist screams of horror and pain that these checks are unnecessary and inefficient. Two things about this question:

  1. Do you know what else is inefficient? Had to send messages to ~~Laurence~~ North Korean hackers via etherscan, transfer money using ETH, and threaten that the FBI would intervene.
  2. You probably already loaded all the data you need from storage, so at the end of the call, just add a little require check to the hot data. Do you want your agreement to cost a negligible fee, or let it die?

If the cost is prohibitive, rethink the core variables, and try to simplify.

What does this mean to me?

As a developer, it is important to define and express core invariants early in the development process. As a concrete suggestion: the first function to get yourself written is _verifyAfter, to verify your invariants after every call to your contract. Put it in your contract, and deploy it there. Supplement this invariant (and other entity-centric invariants) with broader invariant tests that are checked before deployment (Foundry guide).

Transient stores open up some interesting optimizations and improvements that Nascent will be experimenting with -- I suggest you consider how transient stores can be used as a tool for better safety across call contexts.

In this article, not much time is spent on the introduction of the FREI-PI model to input validation, but it is also very important. Defining the bounds of the input is a challenging task to avoid overflow and similar situations. Consider checking out and following the progress of our tool: pyrometer (currently in beta, please give us a star). It can drill down and help find places where you might not be doing input validation.

in conclusion

On top of any catchy acronym (FREI-PI) or schema name, the really important bit is:

Find simplicity in the core immutability of your protocol. And work like hell to make sure it's never destroyed (or caught before it does).

View Original
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
  • Reward
  • Comment
  • Share
Comment
0/400
No comments
Trade Crypto Anywhere Anytime
qrCode
Scan to download Gate app
Community
English
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)