Impermanent Loss Protection using Dynamic Fees based on Market Volatility
Problem: In periods of high volatility and for larger traders, liquidity providers for AMMs (such as Uniswap and Pancakeswap) suffer greater impermanent loss. In TradFi, liquidity providers cover this loss through increasing their fees by widening their bid/ask spreads. However in current AMM models, fees are static. That’s a similar situation for trade volume. This means that customers are overpaying fees in periods of low volatility or for small orders, and that LPs are overexposed to large trades and poor trades in high market volatility environments.
Solution: Using the new V4 Hooks interface from Uniswap and the hooks interface from Pancakeswap, we have implemented smart contracts which provide dynamic fees based on current market volatility and trade size. We use the new Chainlink Oracles for market volatility to obtain trusted decentralized aggregated market data. This market data together with trade order size produce a fee that best reflects the true liquidity at that time using our innovative function based on historical market data. We believe that by charging lower fees, smart routers and customers will be more incentivized to route their orders to LPs implementing our dynamic fees, bringing these LPs more fee money and better execution for customers. We also protect LPs from large trades, like trades done on flash loans, or trades in high volatility periods, such that LPs are able to get larger fees for conditions where they experience greater impermanent loss. This protection will incentivize LPs to contribute more funds, thereby improving market liquidity.
We split our description of the project into the technical foundations as well as the mathematical background of our fee proposal.
--------##### Technical Background #####-------- Regarding the software development, we use the Foundry project framework for structuring and testing Solidity smart contracts. We have implemented both Uniswap and Pancakeswap smart contracts, using the new Hooks interface. We have integrated Chainlink market data oracles into these Hook smart contracts so that the fees are calculated based on the latest market activity, namely the volatility.
--------##### AMM Interface - Uniswap/Pancake Swap #####-------- Our dynamic fee hook smart contract is triggered by the “beforeSwap” event and updates the fee for every swap that occurs within the liquidity pool the hook is attached to. We have added detailed test coverage which validates the use of dynamic fees under different historical market periods and a range of trade order sizes. These tests are deployed against a local running fork of the Sepolia testnet (the Chainlink oracles we are using are currently only available in this testnet). These tests can be run by simply running the command “forge test” from within the Uniswap and Pancakeswap project folders.
--------##### Oracle Interface - Chainlink #####-------- Our Hook smart contract queries the Chainlink market data oracles for information about the 24hr volatility of a given token pair. We use this data to calculate fees based on the latest market activity. Due to the limited availability of pairs, we decided to base our proof of concept on the ETH/USD pair, and use that as a proxy for ETH/USDC volatility as USDC and USD are highly correlated.
We have created a smart contract named MarketData.sol which uses Sepolia Testnet parameters. The contract imports the following interface: AggregatorV3Interface.sol: The source is Chianklink github repository: contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol The interface compiles down to the abi and from where we call the functions
We can use above interface with the addresses of the wanted market data from Sepolia testnet: Currency Pair ETH/USD: Address: 0x694AA1769357215DE4FAC081bf1f309aDC325306 Volatility: ETH/USD 24hr Realized Volatility: address: 0x31D04174D0e1643963b38d87f26b0675Bb7dC96e Once all these data above are known we can write the functions in the MarketData.sol contract to get the market data: getEthUsdPrice(): which calls latestRoundData() function from the interface; getEthUsdVol(): which also calls latestRoundData() function from the interface;
--------##### Unit Testing #####-------- For our unit tests, we created 3 categories: high volatility, mid volatility, and low volatility. Within each volatility category, it’s further subdivided into 3 extra sub-categories: high volume, mid volume, and low volume.
We used Foundry and Forge to write our tests in solidity. All of our tests follow a 3 step process: Arrange: set up variables prior to any swaps Act: retrieve fee calculation and perform swap Assert: Ensure that the fee is calculated as expected, ensure reserves are changed, and ensure we get a specific amount out
We queried all available data on the Chainlink volatility feed and created an excel sheet to sort by volatility. We then used a Sepolia testnet RPC to create a local fork to test our smart contracts on. Afterwards, we picked out timestamps (and thereby block numbers) from our excel sheet which showed periods of high, medium, and low volatility. We forked the chain at these blocks to be able to use our MarketDataProvider contract to go and fetch data from the Chainlink oracles about the price and volatility of ETH/USD.
So overall, we have 3 test files each with 3 functions to test.
--------##### Mathematical Background #####-------- For the mathematical aspect, from our traditional finance backgrounds, we understand that market widths are highly correlated to the volatility, and that larger trades need to pay up more in order to execute successfully. We use orderbook data to calibrate our fee function with volume and volatility.
Due to the difficulty of acquiring crypto spot historical order book data, we collected live orderbook data throughout the day using the Kraken API. From this, we collected 100 samples midday Saturday (see script in our datascience folder in our github repository). From that, we were able to look at the typical liquidity on the orderbook on a relatively quiet day in Ethereum.
From the orderbook data, we saw that on-screen liquidity was plentiful up to around 150 ETH, whereby afterwards it was a bit spotty. Given that, we make an assumption for this project that orders smaller than 150ETH aren’t toxic and that we can linearly charge people less in this bucket, and vice versa for orders larger than 150 lots. We chose to scale fees down to 60% linearly from 100% for orders between 0ETH and 150ETH, and scale up fees with the same slope for orders larger than 150 ETH.
Also, for rough guidance, while calibrating our fee function, we think the fee/ETH at around a swap of 0 ETH can be 3.5 USDC and the fee/ETH at 150ETH at a normal annualized volatility of 60% can be 5.5 USDC (around what is currently being charged on Uniswap, which is 15bps). We use the assumption that 60% is the normal volatility for ETH based on personal experience, but we can explore this dimension further given more time.
Given the lack of available volatility data and further back order book data for spot crypto, we based our market width scaled to variance (volatility squared). This relationship can be fine-tuned in the future with more data.
With what we’ve discussed so far, we fit our fee function to be: 2*(Trade Size / 150 ETH) * (Market Volatility / 60)^2 + 3.5 for ETH/USDC. Details can be found within the datascience directory of our github link. A trade of 0 size will incrementally charge 3.5 USDC/ETH, and a trade of 150 ETH at 60% market volatility will charge 5.5 USDC/ETH, which is what Uniswap will currently charge. Our fee function is monotonically increasing as trade size or market volatility increases, meaning that customers will experience lower fees when they trade less than 150 ETH or when the market is experiencing lower volatility than 60%.
Limitations: Please note that the data we collected was only for a single market, and doesn’t represent the market liquidity, which is likely far greater than what we were able to visualize. Hence, while in our graphs that we saw the order book is able to provide liquidity for 150ETH easily, the market can most likely handle much more than that, but we will maintain our 150ETH assumption for simplicity and for the sake of time.