Solving the issue with slippage in EIP-4626

Nick Addison
mStable
Published in
7 min readNov 10, 2022

Introduction

EIP-4626 provides a standard way to invest a token into an investment pool, which is commonly called a vault. When you deposit your assets, which is an ERC-20 token, you receive a share token that represents your portion of the assets in the vault. The vault will invest the pooled assets into one or more underlying platforms to generate yield for the shareholders. See the full EIP-4626 specification at https://eips.ethereum.org/EIPS/eip-4626.

A consequence of the EIP-4626 standard is that the deposit and mint functions don’t provide a way to specify a minimum share or asset amounts in return. This is commonly used to prevent high slippage or sandwich attacks. How did mStable solve this with its Meta Vaults — staying compliant with the standard while mitigating high slippage attacks? This article describes the challenges and explains how our approach works.

EIP-4626 and mStable vaults deposits

The first of the mStable EIP-4626 vaults will invest in Convex pools based in Curve’s 3Pool. You can read more about the vaults in the proposal here. From an EIP-4626 perspective, the asset of the vault is Curve 3Pool’s liquidity provider token (3Crv). The deposit function is part of the EIP-4626 specification and specifies how many assets are to be deposited and the account that will receive the vault shares. The deposit function returns how many vault shares were minted to the receiver.

function deposit(uint256 assets, address receiver)
external
returns (uint256 shares);

For example, a deposit to the 3Crv Convex mUSD Vault will transfer 3Crv from the caller and mint vcx3CRV-mUSD vault shares to the receiver.

The power of the EIP-4626 standard is there’s a common method to invest in the investment pool but no restrictions on what and when the assets can be invested into underlying platforms. For mStable’s 3Crv Convex mUSD Vault, the 3Crv is added to the Curve mUSD Metapool and the resulting liquidity provider token (musd3Crv) is then deposited into the Convex mUSD pool which invests in the Curve mUSD gauge with boosted rewards.

A technical challenge in this process is how to protect against sandwich attacks.

What are sandwich attacks and how to prevent them?

When you add liquidity to a Curve Metapool (or any other pool for that matter) you specify the amount of assets you want to deposit and the minimum amount of the liquidity provider (LP) tokens you are willing to receive in return (commonly: minimum out). For the mUSD Metapool, the amounts are a two-item array. The first is the amount of mUSD, the second is the amount of 3Crv. The 3Crv Convex vaults are only depositing 3Crv so the first item of the amount array will be zero.

function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount)
external
returns (uint256);

A technical challenge when developing the vaults was how we set the min amount of expected liquidity provider tokens.

Just setting the min_mint_amount to zero is not good enough as it allows any deposit transaction to be sandwich attacked. But before we go into how sandwich attacks work, we need to understand more about how Curve Metapool pricing works. As the vault is only adding one of the two pool tokens (mUSD and 3Crv), the amount of Metapool liquidity provider (LP) tokens it receives will depend on the balance of mUSD and 3Crv in the Metapool. The more 3Crv that is in the pool the fewer LP tokens will be returned when adding just 3Crv to the Metapool.

For example, if Curve’s mUSD Metapool had 2m mUSD, 6m 3Crv and 100k of 3Crv were added, 100,068 LP tokens (musd3Crv) will be received. If the Metapool had 6m mUSD, 2m 3Crv and 100k of 3Crv were added, 100,892 LP tokens (musd3Crv) will be received.

So how would a sandwich attack work?

Attackers watch the Mempool for transactions that can be exploited before they are included in a block. To exploit a transaction, they bribe block producers to include their transaction in front of and after the exploitable transaction. That is, they sandwich the vulnerable transaction with their own transactions. If there was a transaction to add 3Crv to the mUSD Metapool with a zero min LP amount, the attacker’s first transaction would be to decrease the amount of mUSD in the Metapool. This means the amount of Metapool LP tokens received in the vulnerable add liquidity transaction is much less than what it should be. In the third transaction, the attacker returns the mUSD removed in the first transaction and pockets the gains.

Example

Using Curve’s mUSD Metapool starting with 6,000,000 mUSD and 3Crv in the pool, 11,917,295 LP tokens (musd3Crv) and a virtual price of 1.018095 USD.

The attacker uses their majority of the pool’s liquidity provider tokens (musd3Crv) to imbalance the pool by withdrawing 5,973,425 mUSD from the pool using 6,500,000 (54.5%) pool liquidity provider (musd3Crv) tokens. The one-sided withdrawal using theremove_liquidity_one_coin function leaves the pool with 0.43% mUSD and 99.56% 3Crv. The virtual price has increased nearly 1% to 1.019105 as the large imbalancing withdrawal collected fees for the pool.

The victim adds 100,000 3Crv to the imbalanced pool using the add_liquidity function with no min liquidity provider amount. The victim gets 81,978 LP tokens instead of 100,371 if the pool was balanced. That means the victim gets 18,393 (18%) fewer LP tokens than they should. In dollar terms, the victim got 18,643 (18%) less in USD value.

For the third and final transaction, the attacker adds the 5,973,425 mUSD they withdrew with the first transaction back to the pool using add_liquidity to receive 6,503,610 LP tokens (musd3Crv). That's 3,610 more than they withdrew in the first transaction. The pool's virtual price will increase by 1% to 1.019216 as this is another imbalanced transaction. In dollar terms, the attacker's LP value goes from 6,500,000 * 1.018095 = 6,617,617 USD to 6,503,610 * 1.019216 = 6,628,583 USD which is a 10,966 USD (1.65%) increase.

If the victim lost 18,643 in USD value and the attacker only gained 10,966 in USD value, where is the missing 7,677 USD value?

The 0.04% fee on imbalancing the pool is evenly split between liquidity providers and Curve vote-escrowed CRV (veCRV) holders. The 5,417,295 LP tokens not held by the attacker went from a USD value of 5,515,323 to 5,520,794 with the increase in the pool’s virtual price. That’s an increase of 5,471 USD from 50% of the pool’s fees. The reaming USD value goes to the escrow CRV (veCRV) holders.

Curve protections

To protect against a sandwich attack, a fair minimum amount of LP tokens needs to be specified when adding liquidity to the Curve Metapool. Typically, DeFi protocols have a fair amount passed in as part of the transaction. The add_liquidity function of Curve’s pools is a great example with the min_mint_amount. But with the standard EIP-4626 deposit function there is no parameter defined to specify the minimum amount and therefore we can not pass in a fair amount of Metapool LP tokens that were calculated off-chain.

The Curve pools have a calc_token_amount function that calculates the amount of LP tokens received for the deposit of pool tokens. This can not be used to prevent a sandwich attack though. If a prior transaction has already been run to imbalance the pool, the calc_token_amount function will just return the now unfair LP token amount.

function calc_token_amount(uint256[2] memory amounts, bool is_deposit) external view returns (uint256);

So the problem remains, the EIP-4626 function has no way to pass in a minimum amount. Breaking the standard to add this is not an option and using oracles is also suboptimal. We need an on-chain method.

Our approach

The way mStable’s vaults get a fair Metapool LP token price is to use the virtual prices of the Curve Metapool and Curve 3Pool. The get_virtual_price function returns the price of the pool’s liquidity provider token in USD. It does this by calculating the pool's invariant which is the USD value of the tokens in the pool divided by the token’s total supply. As the balance of the tokens in the pool does not affect the pool’s invariant, or total USD value, the virtual price is safe from sandwich attack.

function get_virtual_price() external view returns (uint256);

For the deposit into mStable’s vaults, we need to price the Metapool LP tokens in Curve’s 3Pool LP token (3Crv) as that’s the asset we are using in the vault. To do that we get the 3Pool virtual price and divide it by the Metapool LP token price.

fair Metapool LP tokens = 3Crv assets * 
3Pool virtual price /
Metapool virtual price

Once we have a fair price we can reduce it by a slippage factor which is currently configured to be 1%. This adjusted fair price is used to calculate the minimum number of Curve Metapool LP tokens (musd3Crv) that can be received when adding 3Crv liquidity to the pool.

The full process for a deposit is

Conclusion

While standards play a huge part in standardisation and gaining adoption — problems like these remind us that there are no easy wins when it comes to DeFi. We recognize the limitations of existing standards and find optimal workarounds for them, and in this case, the problem of slippage when it comes to ERC-4626 Vaults.

About mStable

mStable unlocks secure and composable yield for DeFi.

Website | Discord | Twitter | Forum

--

--

mStable
mStable

Published in mStable

mStable builds world leading DeFi products

Nick Addison
Nick Addison

Written by Nick Addison

Smart contracts developer at mStable

No responses yet