
Original title: "Solving the issue with slippage in EIP-4626 》
first level title
Original compilation: ChinaDeFi
Introduction
Introduction
One consequence of the EIP-4626 standard is that the deposit and mint functions do not provide a way to specify a minimum share or asset amount for a return. This is often used to prevent high slippage or sandwich attacks. How does mStable address this issue with its Meta Vaults - mitigating high slippage attacks while remaining compliant? This paper describes these challenges and explains how their approach works.
first level title
EIP-4626 and mStable Gold Vault Deposits
function deposit(uint 256 assets, address receiver)
external
returns (uint 256 shares);
The first vault of mStable EIP-4626 will invest in Convex pool based on Curve 3 Pool. From the perspective of EIP-4626, the vault's assets are Curve 3 Pool's liquidity provider tokens (3 Crv). The deposit function is part of the EIP-4626 specification and specifies how much assets are to be deposited and the account that will receive the treasury share. The deposit function returns to the recipient how many treasury shares will be minted.
How to solve the high slippage problem in EIP-4626?
The power of the EIP-4626 standard is that there is a common approach to investing in investment pools, but there are no restrictions on what and when assets can be invested in the underlying platform. For mStable's 3 Crv Convx mUSD vault, 3 Crv are added to the Curve mUSD Metapool, and the resulting liquidity provider tokens (musd 3 Crv) are deposited into the Convex mUSD pool, which invests in Curve mUSD gauge and get higher rewards.
first level title
What is a sandwich attack? How to prevent them?
function add_liquidity(uint 256 [ 2 ] memory amounts, uint 256 min_mint_amount)
external
returns (uint 256 );
When we add liquidity to Curve Metapool (or any other pool), we specify the amount of assets we want to deposit and the minimum amount of Liquidity Provider (LP) tokens. For the mUSD Metapool, the amount is an array containing two items. The first is the amount of mUSD and the second is the amount of 3 Crv. The 3 Crv Convex vault only holds 3 Crv, so the first item of the amounts array will be zero.
A technical challenge when developing the vault was how we set the minimum number of expected liquidity provider tokens.
For example, if Curve's mUSD Metapool adds 2 million mUSD, 6 million 3 Crv and 100 k 3 Crv, it will receive 100,068 LP tokens (musd 3 Crv). If Metapool has 6 million mUSD, add 2 million 3 Crv and 100 k 3 Crv, will receive 100,892 LP tokens (musd 3 Crv).
first level title
The attacker monitors the Mempool for potentially exploitable transactions before including them in a block. To exploit transactions, they bribe block producers to include their transactions before and after exploitable transactions. That is, they sandwich the vulnerable transaction with their own. If there is a transaction that adds 3 Crv to the mUSD Metapool where the minimum LP amount is zero, the attacker's first transaction will be to decrease the amount of mUSD in the Metapool. This means that the amount of Metapool LP tokens received in vulnerable add liquidity transactions is much lower than it should be. In the third transaction, the attacker returns the mUSD removed in the first transaction and pockets the proceeds.
example
example
Using Curve's mUSD Metapool, there are 6,000,000 mUSD and 3 Crv in the pool, 11,917,295 LP tokens (musd 3 Crv) and a virtual price of $1.018095.
The attacker used most of the LP tokens in their pool by withdrawing 5,973,425 mUSD from the pool using 6,500,000 (54.5%) of the pool's liquidity provider (musd 3 Crv) tokens (musd 3 Crv) to balance the pool. Use the remove_liquidity_one_coin function to make a one-sided withdrawal, leaving 0.43% mUSD and 99.56% 3 Crv in the pool. The dummy price was up almost 1% to 1.019105, as large unbalanced withdrawals charged fees to the pool.
The victim uses the add_liquidity function to add 100,000 3 Crvs to an unbalanced pool with no minimum number of liquidity providers. If the pool is balanced, the victim gets 81978 LP tokens instead of 100371. This means that victims received 18,393 (18%) fewer LP tokens than they should have received. In dollar terms, victims received a reduction of 18,643 (18%) in dollar value.
For the third and final transaction, the attacker uses add_liquidity to add back to the pool the 5,973,425 mUSD they withdrawn from the first transaction to receive 6,503,610 LP tokens (musd 3 Crv ). Withdrawing $3610 more than the first transaction. The virtual price of the pool will increase by 1% to 1.019216 as this is another unbalanced transaction. In USD terms, the attacker's LP value increased from $6,500,000 * 1.018095 = $6,617,617 to $6,503,610 * 1.019216 = $6,628,583, an increase of $10,966 (1.65% ).
The 0.04% fee to unbalance the pool is shared equally between liquidity providers and Curve voting escrowed CRV (veCRV) holders. The value of the 5,417,295 LP tokens not held by the attacker increased from $5,515,323 to $5,520,794. That's an increase of $5,471 over 50% of pool fees. The increased USD value goes to escrowed CRV (veCRV) holders.
first level title
Curve Protection
In order to prevent sandwich attacks, when adding liquidity to Curve Metapool, it is necessary to specify a reasonable minimum number of LP tokens. Usually, DeFi protocols will pass in a considerable amount of money in the transaction. The add_liquidity function in the Curve pool is a good example of min_mint_amount. But for the standard EIP-4626 deposit function, there is no parameter defined to specify the minimum amount, so we cannot pass in a significant amount of off-chain calculated Metapool LP tokens.
function calc_token_amount(uint 256 [ 2 ] memory amounts, bool is_deposit) external view returns (uint 256 );
So the problem remains, there is no way for EIP-4626 functions to pass a minimum amount. Breaking the standard to add this is undesirable, and using an oracle is suboptimal. We need on-chain methods.
first level title
Methods of mStable
function get_virtual_price() external view returns (uint 256 );
The way for mStable's treasury to get a fair price for Metapool LP tokens is to use virtual prices from Curve Metapool and Curve 3 Pool. The get_virtual_price function returns the price of the pool's liquidity provider tokens in USD. It does this by computing an invariant for the pool, which is the dollar value of the tokens in the pool divided by the total supply of tokens. Since the balance of tokens in the pool does not affect the invariant or total dollar value of the pool, virtual prices are immune to sandwich attacks.
fair Metapool LP tokens = 3 Crv assets *
3 Pool virtual price /
Metapool virtual price
For deposits into the mStable vault, we need to price the Metapool LP token in Curve's 3 Pool LP token (3 Crv), as this is the asset we use in the vault. For this, we get the 3 Pool virtual price and divide it by the Metapool LP token price.
Once we have a reasonable price, we can reduce it with a slippage factor currently configured at 1%. This adjusted fair price is used to calculate the minimum amount of Curve Metapool LP tokens (musd 3 Crv) that can be received when adding 3 Crv liquidity to the pool.
in conclusion
in conclusion