-
Notifications
You must be signed in to change notification settings - Fork 11.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement or recommend mitigations for ERC4626 inflation attacks #3706
Comments
the morpho erc4626 vault contract should be in this repo. https://github.com/morpho-dao/morpho-tokenized-vaults |
As the name suggest, the inflation attack comes from the amount of underlying assets changing without the amount of shares reflecting that. This inflation makes all the share more valuable, which means that someone buying shares will get less than they would have expected. The bigger the inflation, the bigger the effect of the attack, which is why this is particularly sensitive when then vault is its early stages, with low liquidity. A few mitigations strategies have been mentioned:
The first solution is the most simple one, but also possibly the less satisfying. It doesn't actually address the issue as much as it goes around it. Advantage:
Drawback:
Keeping track of the assets held by the vault internally remove the effect of direct transfers. Tokens transferred directly are not accounted for (unlike tokens that are explicitly transferred during a This however causes other issues. Transferring tokens is still possible, and if they are not accounted for automatically, they might not be recoverable. To recover them would likely require some form of access control, which might not be desirable when building trust minimizing applications. In addition to direct transfers, this also apply to re-basing tokens! Advantage:
Drawback:
The last option is inspired by Uniswap V2, which created some "dead LP shares" when the first liquidity is deposited. That basically means that the first liquidity provider is giving away a fraction of its shares for monetary stability. A similar approach could be implemented in ERC4626 vaults, by minting "dead shares" on the first In addition to not solving the actual problem (it is still possible to do inflation attacks, the profit is just reduced) this approach disincentivize any early liquidity providing that is not part of an inflation attack. It asks a lot of questions with the amount of "dead shares" to mint that possibly don't have an absolute good answer, and must be answered on a case-by-case basis. Advantage:
Drawback:
Any other solution I should study? |
How they fixed itMorpho-daoAt initialization (eq. construction) deposit some assets into the vault. The corresponding shares are minted to the vault itself, which acts as a dead address. This is similar to the 3rd option above, but with the loss taken by the project. The more is deposited initially, the more difficult it is to perform an inflation attack ... but funds needs to be available for that (they are lost). Note:
YieldBoxWhen computing the exchange rate, add a "virtual" amount of shares and assets to the vault. That is equivalent to the behavior of option 3, but without the need to actually burn any tokens to mint the "dead shares". This fixes the second drawback, but a fraction of any tokens that the vault receives from direct transfer, or re-basing, will be lost. |
There is also another option, though it's arguable a "non-solution": leaving Of the three options presented, I favor tracking the asset balance internally. |
By default, or as an option? I would go for the second, because the internal tracking has a cost, and devs that don't need it should not pay for it. You can override the |
The problem is that the default should be risk-minimized... If we want to make it optional, the default should be abstract. |
I don't fully disagree with that. IMO the default should be feature-minimal, with security issues clearly identified, and option to address the issues through additional feature. That is what we did with Ownable/Ownable2Step. |
Does that mean you would agree with leaving |
I'm not sure about the abstract version. It feels like we purposefully remove one line of code just to force users to read the doc ... |
@Amxx the second option alone (keeping track of the assets) may not be effective to prevent various ways of inflating the price. Especially, adversaries could still donate to the place where the vault assets are deployed and yields are accumulated, e.g., https://www.rileyholterhus.com/writing/bunni IMO, the price inflation is hard to completely prevent, since it is not easy to distinguish genuine vs fake yield. So, I think a separate mitigation to make it hard to profit from price inflation would be still needed, e.g., requiring a minimum initial deposit, or extra slippage protection. |
Is the minimum initial deposit the standard accepted way to mitigate this? It seems like a really suboptimal approach. (It's literally burning money? I guess you can call it a "security budget". 😬) We can adopt Morpho's solution which is really clean and can be easily disabled by users who don't want to make an initial deposit for whatever reason (e.g., testing purposes). |
YieldBox's option is also really nice: It achieve the same result without the need to actually burn/lock any real underlying asset. It also greatly simplify the conversion function fallbacks.
You could override the offset, or use an immutable variable set at construction. This removes the need for |
Can you give a concrete example with numbers of how the virtual offsets would eat part of the profits from a rebasing token? How much would be lost? My guess is the fraction lost would be "assetsOffset / (totalAssets + assetsOffset)"? |
Yes, Which is the same as what would be lost with the current implementation if you deposited "assetsOffset" in the constructor (and locked the corresponding shares). Its the same security, without the "cost". |
I like YieldBox's approach. As @Amxx said, it is essentially a virtual initial deposit that is locked forever (as long as the offset is immutable). So it can effectively mitigate both this issue and #3800 at no cost (zero "security budget")! However, in its current form, I don't think it properly handles the negative interest case. Suppose that a vault deploys its assets to under-collateralized lending protocols (for better APY) and later some of their loans become default, resulting in a loss of principal, i.e., totalAssets < totalSupply. Now, suppose that all the users attempt to withdraw all their (remaining) assets. Then, the last few unlucky users will never be able to withdraw, and their shares will be permanently locked. For example, after the loss of principal, suppose that totalAssets = 1e18, totalSupply = 3e18, and assetOffset = supplyOffset = 1e18, where Alice, Bob, and Charlie's share is equally 1e18 respectively. Suppose Alice and Bob redeem all their shares, then they will get 5e17 respectively. Later, Charlie attempts to redeem his shares, but it will revert because the vault's real balance is zero, and asset transfer will fail (even if convertToAsset(1e18) = 5e17 = 1e18 * (0 + 1e18) / (1e18 + 1e18)). Simply put, the "virtual initial depositor" can only gain but not lose, so some real users (i.e., the last remaining users) will lose more than they should, if not all, in case of negative interest. |
Interesting. |
Interestingly relevent thread for us! BentoBox was pre ERC-4626, but had a somewhat similar protection to the Morpho-dao solution. Yieldbox solution was an attempt to improve on that, initial thoughts were it was a step in the right direction, but we hadn't considered all cases. |
In original YieldBox solution assetsOffset = 1. Look: https://github.com/boringcrypto/YieldBox/blob/master/contracts/YieldBoxRebase.sol#L24 YieldBox solution fixes the issue |
Thanks @Okavango, very important observation. YieldBox uses assetsOffset = 1 and supplyOffset = 1e8. This does seem to be a pretty great solution. The only "issue" is that the number of decimals of the vault token will have 8 more decimals than the assets, which should probably be reflected in |
I agree, it's a great solution! Thanks @Okavango! Now I can see that the assetsOffset = 1 and supplyOffset = 1e8 setup works as an internal fixedpoint representation for shares. So, it's like we have 1 wei assets and 1.00000000 wei shares for the virtual initial deposit. This mitigates the inflation attacks because now we can represent as small as 0.00000001 wei share. This also addresses the issue #3800, without suffering from the negative interest problem because the amount of virtual asset is just 1, so the loss of real users, if any, will never be greater than 1, which means nothing in practice. I may add more test cases to the erc4626-tests to fuzz these specific scenarios more effectively, so that others can find these subtle issues more easily from their vaults. |
Fuzzing would be very nice, we were thinking about that earlier today in conversation with @xaler5. Do you think we can model a general property that a user's deposit of I initially thought this issue was just about slippage and that rounding errors weren't a big part of it, but I can see that I was wrong. My understanding of the attack is now as follows: the attacker mints some shares, then manipulates the share price through a donation, so that a subsequent user deposit of a "large" amount of assets ends up minting a "very small" amount of shares (possibly but not necessarily <1 in real terms), causing the depositor to lose the fractional portion of their shares due to truncation, and allowing the attacker to claim the assets corresponding to that fraction, since they hold the majority of the vault's shares and the losses to precision are distributed proportionaly to shareholders. This is why making 0.00000001 share representable fixes the attack, though it will only do it up to 8 decimals. If this is the case, there should be an alternative fix where a user's deposit is done only up to the amount of assets that can be losslessly represented by an integer number of shares at the current share price. That is, defining |
According to EIP-4626:
So the alternative fix I suggested above would not be compliant. However, we can make it compliant with a few adjustments. Is there something I'm missing that would make this a bad solution?
|
To summarize, my proposal for a fix is as follows: uint256 private _maxRoundingLoss = 0.05e18; // configurable
function deposit(uint256 assets, address receiver) public virtual override returns (uint256) {
require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max");
uint256 shares = previewDeposit(assets);
_deposit(_msgSender(), receiver, assets, shares);
// Check that shares are redeemable for roughly the amount deposited, with minimal rounding error.
uint256 redeemable = previewRedeem(shares);
require(redeemable + _maxRoundingLoss >= assets, "ERC4626: deposit loses assets from rounding");
return shares;
} Note the check after |
Great discussion! Re: fuzzing, yes, I have been thinking of working on that, and will let you know if I find a good way to fuzz such scenarios. Re: the loss, I thought that the 8 decimals precision for shares is enough for practical purposes; e.g., given the victim's deposit of 1000 ETH, the adversaries need to donate at least 100M ETH to steal 1 ETH; 10M ETH to steal 0.1 ETH, 1M ETH to steal 0.01 ETH. In general, they need to put Re: the mint vs deposit, I've indeed thought about a similar thing: the mint() method seems more secure than the deposit() method when the vault doesn't have enough liquidity yet, as it gives users more control of the cost basis and proceeds. I'm not sure if it provides the ultimate security though, especially against the price reset (#3800). That all said, I like your extra check which prevents this slightly different type of slippages.
Footnotes
|
Wait, I think I missed another important effect of virtual offsets for this inflation attack. If we have the virtual offsets (even with just assetsOffset = supplyOffset = 1), the known attack scenario cannot be profitable, since the attacker cannot fully withdraw their donation, because a significant part of the donation goes to the "virtual depositor" and will be locked in the vault forever. Of course, users may still lose their funds if the attacker donates a lot of assets (roughly |
@frangio what about the option of just preserving the ratio when the vault is emptied? You mentioned this in #3800. Something like:
|
On reconsideration, seems like my solution solves the sudden price change problem (outlined in #3800) but not the rounding problem. FWIW, on the rounding problem, I prefer the idea of checking for slippage to the idea of some initial deposit. |
Empty ERC4626 vaults can be manipulated to inflate the price of a share and cause depositors to lose their deposits due to rounding in favor of the vault. In particular, this can be done as a frontrunning attack if the vault is used without specifying a minimum of shares to be received using an ERC4626 router.
We caution users against slippage in the documentation but we could offer concrete mitigations either in code or as recommendations.
Morpho Labs mentions they have made some changes on top of our implementation as a mitigation, we should look into that (where is the code?). See also transmissions11/solmate#178.
The text was updated successfully, but these errors were encountered: