The second case is much more complex. As mentioned, our goal is to test real protocols and real-world scenarios. The plan for this part is to explore an extremely interesting type of Echidna fuzzing:
optimization. This type of analysis allows to maximize certain parameters (i.e., profit) by testing different combinations of input parameters. Such analysis can provide security auditors with the ability to detect not only invariant breaks, but also the vulnerabilities that could allow an attacker to receive abnormal amounts of rewards, even is the overall logic of the contract is not compromised (such as minting incorrect amounts of rewards, fees, etc.)
In our case, we will test the Uniswap V3 swap pool, aiming to find the range of ticks to allocate liquidity that will maximize profit (in case when we perform some predefined swaps). In Uniswap V3, liquidity should be assigned to a specific price range, and the amount of collected fees depends on the number of swaps and the movement of the swap price between the price ticks. So, we plan to allocate the liquidity, perform some swaps, collect the fees and try to maximize them using Echidna's
optimization mode.
[NOTE] Don't be too critical about the quality of the testing code, it's the product of tons of changes made in attempts to achieve optimal performance and demonstrate a real-world scenario. Experimentation with various testing parameters is necessary to achieve this. Remember to expect this complexity when testing any real-life protocol. Fuzzing is not difficult, but does require a lot of experimentation, preliminary testing and time.
Let's describe some steps in
fuzzing-test-2.sol more thoroughly:
• prepare some balances of WETH and DAI— ◦ take 100 ETH, swap them to WETH, then swap half to DAI
here• add liquidity to the WETH/DAI pool (half of our WETH tokens and the corresponding number of DAI) here— ◦ by calling the UniswapV3
mint() function
here— — ◦ the range of ticks is determined by the input parameter
_tickDiff, and this is the parameter that Echidna tries to optimize
• perform multiple swaps (DAI->WETH, WETH->DAI, DAI->WETH) here• put a small amount of liquidity(1) to the pool here (to make the pool recalculate the fees)• call collect_rewards() without actual calling the pool's collect() function, simply analysing our position here. It's enough to estimate the amount of received fees, making our test lighter.In the test, we calculate how much WETH we have placed in the liquidity of the pool. Later, in
collect_rewards(), we calculate the relation between the received WETH fees and the amount of WETH that was put in the pool's liquidity. This helps us identify the most profitable range of ticks.
If we run our test in Hardhat (by uncommenting the lines with debug information
import 'hardhat/console.sol', and
console.log() in
collect_rewards()), we receive: