🎉 Gate xStocks Trading is Now Live! Spot, Futures, and Alpha Zone – All Open!
📝 Share your trading experience or screenshots on Gate Square to unlock $1,000 rewards!
🎁 5 top Square creators * $100 Futures Voucher
🎉 Share your post on X – Top 10 posts by views * extra $50
How to Participate:
1️⃣ Follow Gate_Square
2️⃣ Make an original post (at least 20 words) with #Gate xStocks Trading Share#
3️⃣ If you share on Twitter, submit post link here: https://www.gate.com/questionnaire/6854
Note: You may submit the form multiple times. More posts, higher chances to win!
📅 July 3, 7:00 – July 9,
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:
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:
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:
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:
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:
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).