Elegant and simple! Accessed Uniswap V3 on FMZ with 200 lines of code

FMZ QuantFMZ Quant
25 min read

With the popularity of Defi concept in recent years, Uniswap V3 is one of the most popular topics in the field of decentralized finance (DeFi). As a leading decentralized swap protocol, Uniswap V3 provides a more efficient, safer and better user experience. Now, with only 200 lines of code, traders and developers can access Uniswap V3 on the FMZ platform easily.

FMZ is a quantitative trading platform that supports the development, backtesting and real market deployment of quantitative trading strategies. Its easy-to-use interface and powerful functions are not difficult to understand why FMZ is becoming the first choice of DeFi traders and developers.

The process of integrating Uniswap V3 into FMZ is simple and easy to understand, requiring only 200 lines of code to complete. This means that even if you are new to code, you can easily connect to Uniswap V3 on FMZ and start trading immediately.

FMZ has encapsulated a series of web3 basic functions. Besides Uniswap, it can also encapsulate other DEX exchanges with few codes. Next, let me take you to learn the concepts and technologies in defi application from scratch. Due to the space, the following description is as simple as possible. It may not be very precise, but it is easy to understand.

Uniswap V3 Trade Template disclosed by FMZ platform

The code is as follows:

/* jshint esversion: 7 */

const ABI_Route = '[{"inputs":[{"internalType":"address","name":"_factoryV2","type":"address"},{"internalType":"address","name":"factoryV3","type":"address"},{"internalType":"address","name":"_positionManager","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveMax","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveMaxMinusOne","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveZeroThenMax","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"approveZeroThenMaxMinusOne","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes","name":"data","type":"bytes"}],"name":"callPositionManager","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"paths","type":"bytes[]"},{"internalType":"uint128[]","name":"amounts","type":"uint128[]"},{"internalType":"uint24","name":"maximumTickDivergence","type":"uint24"},{"internalType":"uint32","name":"secondsAgo","type":"uint32"}],"name":"checkOracleSlippage","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint24","name":"maximumTickDivergence","type":"uint24"},{"internalType":"uint32","name":"secondsAgo","type":"uint32"}],"name":"checkOracleSlippage","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct IV3SwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IV3SwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"}],"internalType":"struct IV3SwapRouter.ExactOutputParams","name":"params","type":"tuple"}],"name":"exactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IV3SwapRouter.ExactOutputSingleParams","name":"params","type":"tuple"}],"name":"exactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"factoryV2","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"getApprovalType","outputs":[{"internalType":"enum IApproveAndCall.ApprovalType","name":"","type":"uint8"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"}],"internalType":"struct IApproveAndCall.IncreaseLiquidityParams","name":"params","type":"tuple"}],"name":"increaseLiquidity","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"internalType":"struct IApproveAndCall.MintParams","name":"params","type":"tuple"}],"name":"mint","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"previousBlockhash","type":"bytes32"},{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"positionManager","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"pull","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"}],"name":"swapTokensForExactTokens","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"value","type":"uint256"}],"name":"wrapETH","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]';
const ABI_Pool = '[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"}],\"name\":\"Burn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount0\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount1\",\"type\":\"uint128\"}],\"name\":\"Collect\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount0\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount1\",\"type\":\"uint128\"}],\"name\":\"CollectProtocol\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"paid0\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"paid1\",\"type\":\"uint256\"}],\"name\":\"Flash\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"observationCardinalityNextOld\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"observationCardinalityNextNew\",\"type\":\"uint16\"}],\"name\":\"IncreaseObservationCardinalityNext\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint160\",\"name\":\"sqrtPriceX96\",\"type\":\"uint160\"},{\"indexed\":false,\"internalType\":\"int24\",\"name\":\"tick\",\"type\":\"int24\"}],\"name\":\"Initialize\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"}],\"name\":\"Mint\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"feeProtocol0Old\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"feeProtocol1Old\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"feeProtocol0New\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"feeProtocol1New\",\"type\":\"uint8\"}],\"name\":\"SetFeeProtocol\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"int256\",\"name\":\"amount0\",\"type\":\"int256\"},{\"indexed\":false,\"internalType\":\"int256\",\"name\":\"amount1\",\"type\":\"int256\"},{\"indexed\":false,\"internalType\":\"uint160\",\"name\":\"sqrtPriceX96\",\"type\":\"uint160\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"liquidity\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"int24\",\"name\":\"tick\",\"type\":\"int24\"}],\"name\":\"Swap\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"}],\"name\":\"burn\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"internalType\":\"uint128\",\"name\":\"amount0Requested\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"amount1Requested\",\"type\":\"uint128\"}],\"name\":\"collect\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"amount0\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"amount1\",\"type\":\"uint128\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint128\",\"name\":\"amount0Requested\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"amount1Requested\",\"type\":\"uint128\"}],\"name\":\"collectProtocol\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"amount0\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"amount1\",\"type\":\"uint128\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"factory\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"fee\",\"outputs\":[{\"internalType\":\"uint24\",\"name\":\"\",\"type\":\"uint24\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeGrowthGlobal0X128\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeGrowthGlobal1X128\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"flash\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint16\",\"name\":\"observationCardinalityNext\",\"type\":\"uint16\"}],\"name\":\"increaseObservationCardinalityNext\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint160\",\"name\":\"sqrtPriceX96\",\"type\":\"uint160\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"liquidity\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"maxLiquidityPerTick\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"},{\"internalType\":\"uint128\",\"name\":\"amount\",\"type\":\"uint128\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"mint\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"observations\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"blockTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"int56\",\"name\":\"tickCumulative\",\"type\":\"int56\"},{\"internalType\":\"uint160\",\"name\":\"secondsPerLiquidityCumulativeX128\",\"type\":\"uint160\"},{\"internalType\":\"bool\",\"name\":\"initialized\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32[]\",\"name\":\"secondsAgos\",\"type\":\"uint32[]\"}],\"name\":\"observe\",\"outputs\":[{\"internalType\":\"int56[]\",\"name\":\"tickCumulatives\",\"type\":\"int56[]\"},{\"internalType\":\"uint160[]\",\"name\":\"secondsPerLiquidityCumulativeX128s\",\"type\":\"uint160[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"positions\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"liquidity\",\"type\":\"uint128\"},{\"internalType\":\"uint256\",\"name\":\"feeGrowthInside0LastX128\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"feeGrowthInside1LastX128\",\"type\":\"uint256\"},{\"internalType\":\"uint128\",\"name\":\"tokensOwed0\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"tokensOwed1\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolFees\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"token0\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"token1\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"feeProtocol0\",\"type\":\"uint8\"},{\"internalType\":\"uint8\",\"name\":\"feeProtocol1\",\"type\":\"uint8\"}],\"name\":\"setFeeProtocol\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"slot0\",\"outputs\":[{\"internalType\":\"uint160\",\"name\":\"sqrtPriceX96\",\"type\":\"uint160\"},{\"internalType\":\"int24\",\"name\":\"tick\",\"type\":\"int24\"},{\"internalType\":\"uint16\",\"name\":\"observationIndex\",\"type\":\"uint16\"},{\"internalType\":\"uint16\",\"name\":\"observationCardinality\",\"type\":\"uint16\"},{\"internalType\":\"uint16\",\"name\":\"observationCardinalityNext\",\"type\":\"uint16\"},{\"internalType\":\"uint8\",\"name\":\"feeProtocol\",\"type\":\"uint8\"},{\"internalType\":\"bool\",\"name\":\"unlocked\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"int24\",\"name\":\"tickLower\",\"type\":\"int24\"},{\"internalType\":\"int24\",\"name\":\"tickUpper\",\"type\":\"int24\"}],\"name\":\"snapshotCumulativesInside\",\"outputs\":[{\"internalType\":\"int56\",\"name\":\"tickCumulativeInside\",\"type\":\"int56\"},{\"internalType\":\"uint160\",\"name\":\"secondsPerLiquidityInsideX128\",\"type\":\"uint160\"},{\"internalType\":\"uint32\",\"name\":\"secondsInside\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"zeroForOne\",\"type\":\"bool\"},{\"internalType\":\"int256\",\"name\":\"amountSpecified\",\"type\":\"int256\"},{\"internalType\":\"uint160\",\"name\":\"sqrtPriceLimitX96\",\"type\":\"uint160\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"swap\",\"outputs\":[{\"internalType\":\"int256\",\"name\":\"amount0\",\"type\":\"int256\"},{\"internalType\":\"int256\",\"name\":\"amount1\",\"type\":\"int256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"int16\",\"name\":\"\",\"type\":\"int16\"}],\"name\":\"tickBitmap\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"tickSpacing\",\"outputs\":[{\"internalType\":\"int24\",\"name\":\"\",\"type\":\"int24\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"int24\",\"name\":\"\",\"type\":\"int24\"}],\"name\":\"ticks\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"liquidityGross\",\"type\":\"uint128\"},{\"internalType\":\"int128\",\"name\":\"liquidityNet\",\"type\":\"int128\"},{\"internalType\":\"uint256\",\"name\":\"feeGrowthOutside0X128\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"feeGrowthOutside1X128\",\"type\":\"uint256\"},{\"internalType\":\"int56\",\"name\":\"tickCumulativeOutside\",\"type\":\"int56\"},{\"internalType\":\"uint160\",\"name\":\"secondsPerLiquidityOutsideX128\",\"type\":\"uint160\"},{\"internalType\":\"uint32\",\"name\":\"secondsOutside\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"initialized\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"token0\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"token1\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]'
const ABI_Factory = '[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"indexed\":true,\"internalType\":\"int24\",\"name\":\"tickSpacing\",\"type\":\"int24\"}],\"name\":\"FeeAmountEnabled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token0\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token1\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"indexed\":false,\"internalType\":\"int24\",\"name\":\"tickSpacing\",\"type\":\"int24\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"pool\",\"type\":\"address\"}],\"name\":\"PoolCreated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"tokenA\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"tokenB\",\"type\":\"address\"},{\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"}],\"name\":\"createPool\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"pool\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"internalType\":\"int24\",\"name\":\"tickSpacing\",\"type\":\"int24\"}],\"name\":\"enableFeeAmount\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint24\",\"name\":\"\",\"type\":\"uint24\"}],\"name\":\"feeAmountTickSpacing\",\"outputs\":[{\"internalType\":\"int24\",\"name\":\"\",\"type\":\"int24\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"uint24\",\"name\":\"\",\"type\":\"uint24\"}],\"name\":\"getPool\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"parameters\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"factory\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token0\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token1\",\"type\":\"address\"},{\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"internalType\":\"int24\",\"name\":\"tickSpacing\",\"type\":\"int24\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]'

let ContractV3Factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984"
let ContractV3SwapRouterV2 = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"


function computePoolPrice(decimals0, decimals1, sqrtPriceX96) {
    [decimals0, decimals1, sqrtPriceX96] = [decimals0, decimals1, sqrtPriceX96].map(BigInt);
    const TWO = BigInt(2);
    const TEN = BigInt(10);
    const SIX_TENTH = BigInt(1000000);
    const Q192 = (TWO ** BigInt(96)) ** TWO;
    return (
        Number((sqrtPriceX96 ** TWO * TEN ** decimals0 * SIX_TENTH) / (Q192 * TEN ** decimals1)) /
        Number(SIX_TENTH)
    );
}

function toAmount(s, decimals) {
    return Number((BigDecimal(BigInt(s))/BigDecimal(Math.pow(10, decimals))).toString())
}

function toInnerAmount(n, decimals) {
    return (BigDecimal(n)*BigDecimal(Math.pow(10,decimals))).toFixed(0)
}

$.NewUniswapV3 = function(e) {
    e = e || exchange
    if (e.GetName() !== 'Web3') {
        panic("only support Web3 exchange")
    }


    let self = {
        tokenInfo: {},
        walletAddress: e.IO("address"),
        pool: {}
    }

    // register
    e.IO("abi", ContractV3Factory, ABI_Factory)
    e.IO("abi", ContractV3SwapRouterV2, ABI_Route)

    self.addToken = function(name, address) {
        let ret = e.IO("api", address, "decimals")
        if (!ret) {
            throw "get token decimals failed"
        }
        let decimals = Number(ret)
        self.tokenInfo[name] = {
            name: name,
            decimals: decimals,
            address: address
        }
    }
    self.waitMined = function(tx) {
        while (true) {
            Sleep(1000)
            let info = e.IO("api", "eth", "eth_getTransactionReceipt", tx)
            if (info && info.gasUsed) {
                return true
            }
            Log('Transaction not yet mined', tx)
        }
    }

    self.swapToken = function(tokenIn, amountInDecimal, tokenOut, options) {
        // options like {gasPrice: 11, gasLimit: 111, nonce: 111}
        let tokenInInfo = self.tokenInfo[tokenIn]
        let tokenOutInfo = self.tokenInfo[tokenOut]
        if (!tokenInInfo) {
            throw "not found token info " + tokenIn
        }

        if (!tokenOutInfo) {
            throw "not found token info " + tokenOut
        }

        let amountIn = toInnerAmount(amountInDecimal, tokenInInfo.decimals)
        let recipientAddress = self.walletAddress
        if (tokenInInfo.name != 'ETH') {
            let allowanceAmount = e.IO("api", tokenInInfo.address, "allowance", self.walletAddress, ContractV3SwapRouterV2);
            let realAmount = toAmount(allowanceAmount, tokenInInfo.decimals)
            if (realAmount < toAmount(amountIn, tokenInInfo.decimals)) {
                Log("realAmount is", realAmount, "too small, try to approve large amount")
                if (tokenInInfo.name == 'USDT') {
                    // As described in Tether code: To change the approve amount you first have to reduce the addresses allowance to 0 calling approve(spender, 0)
                    let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, 0)
                    if (!txApprove) {
                        throw "approve error"
                    }
                    Log("wait reduce approve", txApprove)
                    self.waitMined(txApprove)
                }

                let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
                if (!txApprove) {
                    throw "approve error"
                }
                Log("wait approve", txApprove)
                self.waitMined(txApprove)
                Log("approve success amountIn", amountIn)
            } else {
                Log("allowance", realAmount, "no need to approve")
            }
        }

        if (tokenOutInfo.name == 'ETH' || tokenOutInfo.address.toLowerCase() == '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
            /*
            ADDRESS_THIS https://degencode.substack.com/p/uniswapv3-multicall
            https://etherscan.io/address/0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45#code
            */
            recipientAddress = '0x0000000000000000000000000000000000000002'
        }

        let swapToken = e.IO("encode", ContractV3SwapRouterV2, "swapExactTokensForTokens", amountIn, 1, [tokenInInfo.address, tokenOutInfo.address], recipientAddress)
        let data = [swapToken]
        if (tokenOutInfo.name == 'ETH') {
            data.push(e.IO("encode", ContractV3SwapRouterV2, "unwrapWETH9(uint256,address)", 1, self.walletAddress))
        }
        let tx = e.IO("api", ContractV3SwapRouterV2, "multicall(uint256,bytes[])", (tokenInInfo.name == 'ETH' ? amountIn : 0), (new Date().getTime() / 1000) + 3600, data, options || {})
        if (tx) {
            Log("tx: ", tx)
            self.waitMined(tx)
            Log("swap", tokenInInfo.name, "to", tokenOutInfo.name, "success")
            return true
        } else {
            Log("trans error")
            return false
        }
    }

    self.getETHBalance = function(address) {
        return toAmount(e.IO("api", "eth", "eth_getBalance", address || self.walletAddress, "latest"), 18)
    }

    self.balanceOf = function(token, address) {
        let tokenInfo = self.tokenInfo[token]
        if (!tokenInfo) {
            throw "not found token info " + token
        }

        return toAmount(e.IO("api", tokenInfo.address, "balanceOf", address || self.walletAddress), tokenInfo.decimals)
    }

    self.sendETH = function(to, amount, options) {
        return e.IO("api", "eth", "send", to, toInnerAmount(amount, 18), options || {})
    }

    self.getPrice = function(pair) {
        let arr = pair.split('_')
        let token0 = self.tokenInfo[arr[0]]
        if (!token0) {
            throw "token " + arr[0] + "not found"
        }
        let token1 = self.tokenInfo[arr[1]]
        if (!token1) {
            throw "token " + arr[1] + "not found"
        }
        let reverse = false
        if (BigInt(token0.address) > BigInt(token1.address)) {
            let tmp = token0
            token0 = token1
            token1 = tmp
            reverse = true
        }
        let key = token0.address + '/' + token1.address
        if (typeof(self.pool[key]) == 'undefined') {
            let pool = e.IO("api", ContractV3Factory, "getPool", token0.address, token1.address, 3000)
            if (pool) {
                self.pool[key] = pool
                // register pool address
                e.IO("abi", pool, ABI_Pool)
            }
        }
        if (typeof(self.pool[key]) == 'undefined') {
            throw "pool " + pair + " not found"
        }

        let slot0 = e.IO("api", self.pool[key], "slot0")

        if (!slot0) {
            return null
        }

        let price = computePoolPrice(token0.decimals, token1.decimals, slot0.sqrtPriceX96)
        if (reverse) {
            price = 1 / price
        }
        return price
    }

    return self
}

$.testUniswap = function() {
    let ex = $.NewUniswapV3()
    Log("walletAddress: ", ex.walletAddress)
    let tokenAddressMap = {
        "ETH": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH
        "USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7",
        "1INCH": "0x111111111117dC0aa78b770fA6A738034120C302",
    }
    for (let name in tokenAddressMap) {
        ex.addToken(name, tokenAddressMap[name])
    }

    Log(ex.getPrice('ETH_USDT'))
    Log(ex.getPrice('1INCH_USDT'))
    // swap 0.01 ETH to USDT
    Log(ex.swapToken('ETH', 0.01, 'USDT'))
    let usdtBalance = ex.balanceOf('USDT')
    Log("balance of USDT", usdtBalance)
    // swap reverse
    Log(ex.swapToken('USDT', usdtBalance, 'ETH'))

    Log("balance of ETH", ex.getETHBalance())

    // Log(ex.sendETH('0x11111', 0.02))
}

Ethereum network

Ethereum network can be understood as a software infrastructure, on which various smart contracts can be deployed and run. Smart contracts have various functions and application scenarios. The devices running the Ethereum client constitute the nodes in the Ethereum network.

Some concepts in Uniswap V3

Students who are not familiar with the Uniswap V3 protocol need to understand a few concepts first. Uniswap V3 is also a smart contract deployed and running on Ethereum.

  1. Route: Route is also a smart contract, which is used to manage the exchange of token.

  2. Pool: The pool is also a smart contract for storing two kinds of Ethereum tokens and exchanging between these two tokens.

  3. Factory contract: A factory contract is a smart contract used to create a pool.

  4. ABI: (Application Binary Interface) is a specification that describes how smart contracts communicate with the outside world. It specifies the function name, parameter type and return value type of the smart contract, how to encode and decode data, and determines the external interface of the smart contract. It can be understood that to call an interface, it must be called according to the agreed standards of the interface, and the ABI records a series of agreed standards.

Once the smart contract is deployed on Ethereum, there will be an address.

Analyze the code of Uniswap V3 Trade Template

The Uniswap V3 trading class library code is mainly divided into four parts, which we will explain one by one.

Part 1: Constants used when interacting with Uniswap V3

const ABI_Route = '[{"inputs":[{"internalType":"address...
const ABI_Pool = '[{\"inputs\":[],\"stateMutability\":\"nonpayable...
const ABI_Factory = '[{\"inputs\":[],\"stateMutability\":\"...

let ContractV3Factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984"
let ContractV3SwapRouterV2 = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"

With the above basic concept preview, it's easy to understand here.

ABI_Route The string stored by this constant is the ABI of the routing smart contract.
ABI_Pool stores the ABI of the pool contract.
ABI_Factory The ABI of the factory contract.

Since these strings are long, they are just excerpts. These contents provide a standard for the program to call the smart contract method (for example, what are the parameters of the smart contract interface, how many parameters, what type are they, and what type of returned data are).

As we mentioned just now, once the smart contract is deployed in Ethereum, there will be an address.

ContractV3Factory: The address of the factory contract is recorded.
ContractV3SwapRouterV2: The router V2 address of Uniswap V3. Note that Uniswap has V1 and V2. The router of Uniswap V3 also has V1 and V2. Different contract addresses are different.

Part2: Tool functions

  1. computePoolPrice function is used to calculate the price of the token in the pool.
function computePoolPrice(decimals0, decimals1, sqrtPriceX96) {
    [decimals0, decimals1, sqrtPriceX96] = [decimals0, decimals1, sqrtPriceX96].map(BigInt);   // Use the BigInt function for processing. Because of the precision of JavaScript language values, it is necessary to use one of FMZ's underlying processing functions, BigInt, to process.
    const TWO = BigInt(2);     // Define constant 2 to calculate
    const TEN = BigInt(10);    // Define constant 10 to calculate
    const SIX_TENTH = BigInt(1000000);    // Define the 6th power of constant 10, i.e. 1e6
    const Q192 = (TWO ** BigInt(96)) ** TWO;  // 2^192
    return (
        Number((sqrtPriceX96 ** TWO * TEN ** decimals0 * SIX_TENTH) / (Q192 * TEN ** decimals1)) /
        Number(SIX_TENTH)
    );
}

If the trading pair is ETH_USDT, then token0 is ETH, and token1 is USDT. decimals0 is the precision data of token0, and decimals1 is the precision data of token1. SqrtPriceX96 is the price-related data (not the direct price value), which can be obtained from the slot0 method of the pool contract.

sqrtPriceX96 : The current price of the pool as a sqrt(token1/token0) Q64.96 value
Q64.96 is a data processing and storage standard.

These three data decimals0, decimals1, sqrtPriceX96 are passed into the computePoolPrice function as parameters to calculate the price of the trading pair ETH_USDT. The algorithm in the last return statement of this function is the process of restoring sqrtPriceX96 to token1/token0. For example, the number of token0 (ETH) in the pool is 1, and the number of token1 (USDT) is 1100. So 1100/1=1100, the current price in the ETH_USDT pool is 1100.

  1. toAmount function is used to convert numeric data in the chain into readable data.
function toAmount(s, decimals) {
    return Number((BigDecimal(BigInt(s))/BigDecimal(Math.pow(10, decimals))).toString())
}

To put it simply, for example, an ETH token is 1e18 when the number is represented on the chain, i.e., the 18th power of 10, because the precision data of ETH is 18. Not all tokens have a precision of 18, and the precision of USDT is not the same as that of ETH. The toAmount function is to convert 1e18 to 1.

  1. The toInnerAmount function is the opposite of the toAmount function, which converts readable data into values used in the chain.
function toInnerAmount(n, decimals) {
    return (BigDecimal(n)*BigDecimal(Math.pow(10,decimals))).toFixed(0)
}

Next, let's analyze the code of "Uniswap V3 Trade Template".

Part3: Constructor of Uniswap V3 operation object

The core of this template class library is the Uniswap V3 operation object, which implements the basic operations on Uniswap V3. More functions may be upgraded in the future. By analyzing this code example, even if you do not use the FMZ platform, you will increase your knowledge and understanding of the processes and details of each link of Uniswap DEX. Now let's learn how these basic functions are designed and implemented on FMZ.

Constructor code of Uniswap V3 operation object:

$.NewUniswapV3 = function(e) {
    e = e || exchange                          // If the parameter e is not passed, the exchange object exchange is used, that is, the first exchange added in the strategy.
    if (e.GetName() !== 'Web3') {              // Determine if the exchange object is Web3, because this template only supports Web3 exchange objects.
        panic("only support Web3 exchange")
    }


    let self = {                               // The current function is a constructor, and the constructed object is the object self.
        tokenInfo: {},                         // Member variable of the self object, used to record the registration information of the token.
        walletAddress: e.IO("address"),        // Record the address of the wallet to which the current exchange object is bound.
        pool: {}                               // Used to record registered pool information.
    }

    // register
    e.IO("abi", ContractV3Factory, ABI_Factory)       // ABI for registered factory contracts
    e.IO("abi", ContractV3SwapRouterV2, ABI_Route)    // ABI for registered routing contracts

    self.addToken = function(name, address) {         // Used to register token
        let ret = e.IO("api", address, "decimals")    // Call the decimals method to get token precision information
        if (!ret) {
            throw "get token decimals failed"
        }
        let decimals = Number(ret)
        self.tokenInfo[name] = {
            name: name,
            decimals: decimals,
            address: address
        }
    }
    self.waitMined = function(tx) {             // It is used to wait for the result of an operation on Ethereum. The hash is a tx parameter.
        while (true) {
            Sleep(1000)
            let info = e.IO("api", "eth", "eth_getTransactionReceipt", tx)  // Query results using eth_getTransactionReceipt method, if it's not found, loop to continue query.
            if (info && info.gasUsed) {
                return true
            }
            Log('Transaction not yet mined', tx)
        }
    }

    self.swapToken = function(tokenIn, amountInDecimal, tokenOut, options) {   // For token exchange
        // options like {gasPrice: 11, gasLimit: 111, nonce: 111}
        let tokenInInfo = self.tokenInfo[tokenIn]      // Get the information of the exchanged token
        let tokenOutInfo = self.tokenInfo[tokenOut]    // Get the information of the converted token
        if (!tokenInInfo) {
            throw "not found token info " + tokenIn
        }

        if (!tokenOutInfo) {
            throw "not found token info " + tokenOut
        }

        let amountIn = toInnerAmount(amountInDecimal, tokenInInfo.decimals)  // Convert to data used on smart contracts
        let recipientAddress = self.walletAddress
        if (tokenInInfo.name != 'ETH') {
            let allowanceAmount = e.IO("api", tokenInInfo.address, "allowance", self.walletAddress, ContractV3SwapRouterV2);   // Query the number of authorizations
            let realAmount = toAmount(allowanceAmount, tokenInInfo.decimals)
            if (realAmount < toAmount(amountIn, tokenInInfo.decimals)) {    // If the number of licenses is insufficient
                Log("realAmount is", realAmount, "too small, try to approve large amount")
                if (tokenInInfo.name == 'USDT') {
                    // As described in Tether code: To change the approve amount you first have to reduce the addresses allowance to 0 calling approve(spender, 0)
                    let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, 0)  // If the authorized token is USDT, it needs to be authorized to 0 first.
                    if (!txApprove) {
                        throw "approve error"
                    }
                    Log("wait reduce approve", txApprove)
                    self.waitMined(txApprove)
                }

                let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');  // Authorize Router contracts to operate the wallet's tokens
                if (!txApprove) {
                    throw "approve error"
                }
                Log("wait approve", txApprove)
                self.waitMined(txApprove)
                Log("approve success amountIn", amountIn)
            } else {
                Log("allowance", realAmount, "no need to approve")
            }
        }

        if (tokenOutInfo.name == 'ETH' || tokenOutInfo.address.toLowerCase() == '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
            /*
            ADDRESS_THIS https://degencode.substack.com/p/uniswapv3-multicall
            https://etherscan.io/address/0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45#code
            */
            recipientAddress = '0x0000000000000000000000000000000000000002'
            // When exchanging other coins for WETH, you have to let the contract hold WETH before you can redeem it.
        }

        let swapToken = e.IO("encode", ContractV3SwapRouterV2, "swapExactTokensForTokens", amountIn, 1, [tokenInInfo.address, tokenOutInfo.address], recipientAddress)   // Packaged swapExactTokensForTokens to call.
        let data = [swapToken]
        if (tokenOutInfo.name == 'ETH') {    // If the token exchanged back is ETH, which in this case is actually WETH, then it needs to unpack.
            data.push(e.IO("encode", ContractV3SwapRouterV2, "unwrapWETH9(uint256,address)", 1, self.walletAddress))   // So here is another unwrapWETH9 unpacking to call
        }
        let tx = e.IO("api", ContractV3SwapRouterV2, "multicall(uint256,bytes[])", (tokenInInfo.name == 'ETH' ? amountIn : 0), (new Date().getTime() / 1000) + 3600, data, options || {})   // Use multicall to perform these packaged operations (swapExactTokensForTokens, unwrapWETH9)
        if (tx) {
            Log("tx: ", tx)
            self.waitMined(tx)
            Log("swap", tokenInInfo.name, "to", tokenOutInfo.name, "success")
            return true
        } else {
            Log("trans error")
            return false
        }
    }

    self.getETHBalance = function(address) {   // Check your wallet's ETH balance
        return toAmount(e.IO("api", "eth", "eth_getBalance", address || self.walletAddress, "latest"), 18)
    }

    self.balanceOf = function(token, address) {  // Query the balance of a token in a wallet (determined by parameters)
        let tokenInfo = self.tokenInfo[token]
        if (!tokenInfo) {
            throw "not found token info " + token
        }

        return toAmount(e.IO("api", tokenInfo.address, "balanceOf", address || self.walletAddress), tokenInfo.decimals)
    }

    self.sendETH = function(to, amount, options) {   // Sending ETH tokens to an address, i.e. transfer
        return e.IO("api", "eth", "send", to, toInnerAmount(amount, 18), options || {})
    }

    self.getPrice = function(pair, fee) {     // Get price of trading pair
        let arr = pair.split('_')
        let token0 = self.tokenInfo[arr[0]]
        if (!token0) {
            throw "token " + arr[0] + "not found"
        }
        let token1 = self.tokenInfo[arr[1]]    // First, get the two token information that constitute the trading pair
        if (!token1) {
            throw "token " + arr[1] + "not found"
        }
        let reverse = false
        if (BigInt(token0.address) > BigInt(token1.address)) {
            let tmp = token0
            token0 = token1
            token1 = tmp
            reverse = true
        }
        let key = token0.address + '/' + token1.address
        if (typeof(self.pool[key]) == 'undefined') {
            let pool = e.IO("api", ContractV3Factory, "getPool", token0.address, token1.address, typeof(fee) === 'number' ? fee : 3000)   // Call the getPool method of the factory contract to obtain the address of the exchange pool
            if (pool) {
                self.pool[key] = pool    // Register the pool address and register the ABI of the pool contract
                // register pool address
                e.IO("abi", pool, ABI_Pool)
            }
        }
        if (typeof(self.pool[key]) == 'undefined') {
            throw "pool " + pair + " not found"
        }

        let slot0 = e.IO("api", self.pool[key], "slot0")  // Call the slot0 method of the pool contract to get price related information

        if (!slot0) {
            return null
        }

        let price = computePoolPrice(token0.decimals, token1.decimals, slot0.sqrtPriceX96)  // Calculate the readable price
        if (reverse) {
            price = 1 / price
        }
        return price
    }

    return self
}

Students who may be not familiar with FMZ see the function $.NewUniswapV3 has a strange name. Functions with $. at the beginning indicates that this function is the interface function of the template class library on FMZ (what is the template class library can be [viewed](https://www.fmz.com/api#template - library), simply put, the $.NewUniswapV3 function can be called by other strategies that reference the template class library directly. The strategy has the function of Uniswap V3 directly.

The function $.NewUniswapV3 directly constructs and creates an object, which can be used to perform some operations:

  • Token exchange: implemented by the swapToken method of the object.

  • ETH balance query: implemented by the getETHBalance method of the object.

  • Token balance query: implemented by the balanceOf method of the object.

  • Transaction pair price query: implemented by the getPrice method of the object.

  • Send ETH for transfer: implemented by the sendETH method of the object.

This class library may not be limited to these functions in the future, and may even upgrade and add "add liquidity" and other functions. Let's continue to analyze the code:

    e = e || exchange
    if (e.GetName() !== 'Web3') {
        panic("only support Web3 exchange")
    }


    let self = {
        tokenInfo: {},
        walletAddress: e.IO("address"),
        pool: {}
    }

    // register
    e.IO("abi", ContractV3Factory, ABI_Factory)
    e.IO("abi", ContractV3SwapRouterV2, ABI_Route)

The constructor $.NewUniswapV3 has only one parameter e, which means the exchange object (the exchange configuration on FMZ). Because the strategy on FMZ can be designed to be multi-exchange, so if a specific exchange is passed here, it means that the Uniswap V3 object created is the one that operates the exchange object. If the parameter e is not passed, the first added exchange object is operated by default.

Configure the node service address, private key (you can deploy the private key locally, local deployment only uses the configuration path), and an exchange object is created. It can be added to the strategy at the time of the real market, this object is reflected in the strategy code is exchange also known as exchanges[0], if you add the second, it's exchanges[1], add the third for exchanges[2], ...

The node address I configured in the screenshot: https://mainnet.infura.io/v3/xxx, which is an infura node, which can be applied by individuals. Each account has its own specific address. xxx here is the mask, and the xxx part of each account is different.

Continue with the code. The constructor starts to determine whether the exchange object is Web3, if not, an error will be reported. Then a variable self is created. The self is the object finally returned by the constructor. Subsequent constructors add various functions to this object and implement specific functions. The variable self has three attributes:

  • tokenInfo: records the token information registered in the object, including token address, token precision, and token name.

  • walletAddress: the wallet address of the current exchange object.

  • pool: the exchange pool information registered in the object, mainly including the name and address of the exchange pool.

Then we use the concept we learned in the previous chapter:

e.IO("abi", ContractV3Factory, ABI_Factory)     // Register ABI for Uniswap V3 factory contract
e.IO("abi", ContractV3SwapRouterV2, ABI_Route)  // Register ABI for Uniswap Router V2 routing

Why register this interface information?

Because some functions to be implemented in the future need to call the interfaces of these smart contracts. Next, the constructor adds various methods to the self object. In addition to the above mentioned self object methods: exchange token, query balance, etc., there are also some tool functions belonging to the self object. Let's analyze these tool functions first.

Tool functions for self objects

  1. self.addToken = function(name, address)

By observing the specific code of this function, we can see that this function is used to add (in other words, register) a token information to the current object self in the member tokenInfo that records token information. Because the precision data of token is often used in subsequent calculations, when this function adds (registers) token information, it calls the let ret=e.IO ("api", address, "decimals") function, and calls the "decimals" method of token contract through the FMZ encapsulated exchange.IO function (we mentioned that e is the exchange object passed in) to obtain the precision of token.

So self.tokenInfo is a dictionary structure. Each key name is the token name, and the key value is the token information, including address, name, and precision. It looks like this:

{
    "ETH": {name: "ETH", decimals: 18, address: "0x..."},
    "USDT": {name: "USDT", decimals: 6, address: "0x..."},
    ...
}
  1. self.waitMined = function(tx)

This function is used to wait for the execution result of the smart contract on Ethereum. From the implementation code of this function, we can see that this function has been calling let info=e.IO ("api", "eth", "eth_getTransactionReceipt", tx) in a loop. By calling the RPC method eth_GetTransactionReceipt of Ethereum to query the transaction hash and return the transaction receipt. The parameter tx is the transaction hash.

eth_GetTransactionReceipt and other relevant data can be viewed at: https://ethereum.org/zh/developers/docs/apis/json-rpc/#eth_gettransactionreceipt

Some students may ask: Why do we need this function?

Answer: When performing some operations, such as token exchange, it is necessary to wait for the result.

Next we will look at the other main functions of the object self created by the $.NewUniswapV3 function, we start with the simplest one.

Main function functions

  1. self.getETHBalance = function(address)

The query of token balance can be divided into the query of ETH balance and the query of other ERC20 token balance. The getETHBalance function of the self object is used to query the ETH balance. When the specific wallet address parameter address is passed in, the ETH balance of this address is queried. If the address parameter is not passed, then the ETH balance of the self.walletAddress address (i.e. the wallet configured on the current exchange) is queried.

If the address parameter is not passed, then the ETH balance of the self.walletAddress address (i.e. the wallet configured on the current exchange) is queried.

These are achieved by calling the RPC method eth_getBalance of Ethereum.

  1. self.balanceOf = function(token, address)

To query the token balance other than ETH, you need to pass in the parameter token, that is, the token name, such as USDT. Pass in the address of the wallet to be queried, and if no address is passed in, query the balance of the address of self.walletAddress. By observing the code implemented by this function, we can see that only the token registered through the self.addToken function can be queried, because the precision information and address of the token (token) are required when calling the balanceOf method of the token contract.

  1. self.sendETH = function(to, amount, options)

The function of this function is ETH transfer. To transfer a certain amount of ETH to a wallet address (using the parameter to to set), you can set another options parameter (data structure: {gasPrice: 111, gasLimit: 111, nonce: 111}) to specify the gasLimit/gasPrice/once parameter. The system default setting is used without passing in the options parameter.

GasLimit/gasPrice affects the ETH consumed when performing operations on Ethereum (some operations on Ethereum consume gas, that is, certain ETH tokens).

  1. self.getPrice = function(pair, fee)

This function is used to obtain the price of a trading pair on Uniswap. You can see from the function implementation code that the trading pair will be parsed at the beginning of the function execution to obtain the baseCurrency and quoteCurrency. For example, the trading pair is ETH_USDT, it will be split into ETH and USDT. Then query whether there are two kinds of token information in self.tokenInfo. If there is no information, an error will be reported.

The exchange pool address on Uniswap is composed of two token addresses and Fee (rate standard) calculations. So when querying the pool address recorded in self.pool (we have mentioned self.pool before, you can check it), if not found, use the two token addresses and Fee to calculate the pool address. So a trading pair may have multiple pools, because the Fee may be different.

The address of the query and calculation exchange pool is obtained by calling the getPool method of the factory contract in Uniswap V3 (so you need to register the ABI of the factory contract at the beginning).

Get the pool address of the trading pair, and you can register the ABI of the pool contract. In this way, the slot0 method of this pool (smart contract) can be called to get the price data. Of course, the data returned by this method is not a human-readable price, but a price-related data structure. Further processing is needed to obtain a readable price. At this time, we use the computePoolPrice function mentioned in the previous section.

  1. self.swapToken = function(tokenIn, amountInDecimal, tokenOut, options)

The function of this function is token exchange. The parameter tokenIn is the name of the token paid during exchange, the parameter tokenOut is the name of the token obtained during exchange, and the parameter amountInDecimal is the exchange quantity (human-readable quantity). The parameter options, as we mentioned earlier, can be set for gas consumption, nonce, etc. during exchange.

When the function is executed, the information of token is first obtained from the self.tokenInfo variable. There are many details of the exchange. First, if the token involved in the exchange is not ETH, then the routing (smart contract responsible for exchange) needs to be authorized first. Before authorization, check whether there is enough authorization.

let allowanceAmount = e.IO("api", tokenInInfo.address, "allowance", self.walletAddress, ContractV3SwapRouterV2);

Use the token contract allowance method to query the authorized amount. By comparing the authorized amount with the current amount of exchange, if the authorized amount is enough to exchange, no authorization is needed. If the amount is insufficient, the authorization processing will be executed.

There is also a detail of authorization here. If the authorized token is a USDT, you need to reset the number of authorization to 0 before authorization. The approve method of the token contract is authorized. Note that the approval authorization method is a gas consumption method, which will consume a certain amount of ETH. So you need to use the self.waitMined function to wait for the processing result.

In order to avoid frequent authorization and pay unnecessary ETH, this authorization operation is the maximum one-time authorization.

let txApprove = e.IO("api", tokenInInfo.address, "approve", ContractV3SwapRouterV2, '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');

With enough exchange limits, you can exchange. However, there are also details here. If the token involved in the exchange, the token obtained after the exchange is ETH, then you need to change the receiving address:

recipientAddress = '0x0000000000000000000000000000000000000002'

The specific reasons are more complex and are not described here, please refer to:

ADDRESS_THIS https://degencode.substack.com/p/uniswapv3-multicall
https://etherscan.io/address/0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45#code

Then use the packaging function e.IO("encode", ...) encapsulated by the FMZ platform to package the swapExactTokensForTokens method call for routing (smart contract). If the token obtained after exchange is ETH, you need to add a step of WETH9 unpacking operation:

data.push(e.IO("encode", ContractV3SwapRouterV2, "unwrapWETH9(uint256,address)", 1, self.walletAddress))

Because WETH is involved in the exchange. This is a packaged token of ETH. For real ETH, unpacking operation is required. After the unpacking operation is also packaged, the multicall method of routing (smart contract) can be called to perform this series of operations. Here is another detail to pay extra attention to: if the trading pair participating in the exchange, the payment token is ETH, the number of ETH transferred needs to be set in the following steps. If it is not ETH, set 0.

let tx = e.IO("api", ContractV3SwapRouterV2, "multicall(uint256,bytes[])", (tokenInInfo.name == 'ETH' ? amountIn : 0), (new Date().getTime() / 1000) + 3600, data, options || {})

The setting is reflected here: (tokenInInfo.name=='ETH '? amountIn : 0). I did not make it clear before that I did not set 0 when tokenIn was not equal to ETH token, resulting in the wrong transfer of ETH. So be careful when writing the transfer code.

Part4: How to use Uniswap V3 operation objects

The code in this template is actually less than 200 lines in terms of function implementation. The following paragraph is a demonstration of its use.

$.testUniswap = function() {
    let ex = $.NewUniswapV3()
    Log("walletAddress: ", ex.walletAddress)
    let tokenAddressMap = {
        "ETH": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH
        "USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7",
        "1INCH": "0x111111111117dC0aa78b770fA6A738034120C302",
    }
    for (let name in tokenAddressMap) {
        ex.addToken(name, tokenAddressMap[name])
    }

    Log(ex.getPrice('ETH_USDT'))
    Log(ex.getPrice('1INCH_USDT'))
    // swap 0.01 ETH to USDT
    Log(ex.swapToken('ETH', 0.01, 'USDT'))
    let usdtBalance = ex.balanceOf('USDT')
    Log("balance of USDT", usdtBalance)
    // swap reverse
    Log(ex.swapToken('USDT', usdtBalance, 'ETH'))

    Log("balance of ETH", ex.getETHBalance())

    // Log(ex.sendETH('0x11111', 0.02))
}

The function $.testUniswap=function() is only a demonstration. Do not call it without practical use. Let's use this function to see how to use this template class library to operate the function of Uniswap V3.

In the code, execute let ex=$.NewUniswapV3() to construct a Uniswap V3 operation object first. If you want to get the wallet address bound by the current exchange, you can use ex.walletAddress to get it. Then, the code uses the ex.addToken to register three kinds of tokens, namely ETH, USDT and 1INCH.

Print the price of a trading pair (token needs to be registered first):

Log(ex.getPrice('ETH_USDT'))
Log(ex.getPrice('1INCH_USDT'))

The getPrice function uses the default rate of 3,000 if no Fee is set, which is converted to a readable value of 0.3%.

If you want to convert 0.01 ETH to USDT, then check the balance and then convert back, use the code:

Log(ex.swapToken('ETH', 0.01, 'USDT'))

let usdtBalance = ex.balanceOf('USDT')   // Check the balance of USDT after exchange
Log("balance of USDT", usdtBalance)

Log(ex.swapToken('USDT', usdtBalance, 'ETH'))  // Exchange USDT to ETH
Log("balance of ETH", ex.getETHBalance())      // Check ETH balance

// Log(ex.sendETH('0x11111', 0.02))            // ETH transfer operations

Testing with the test network Goerli

  1. Configure the test network exchange object

Note that setting up the node requires setting up the node as a test network Goerli.

  1. Write a strategy and test it on the test network Goerli.
function main() {
    let ex = $.NewUniswapV3()
    Log("walletAddress: ", ex.walletAddress)
    let tokenAddressMap = {
        "ETH"  : "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",  // WETH
        "LINK" : "0x326C977E6efc84E512bB9C30f76E30c160eD06FB",
        "UNI"  : "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
    }
    for (let name in tokenAddressMap) {
        ex.addToken(name, tokenAddressMap[name])
    }

    // ETH_UNI 、 UNI_ETH
    Log("ETH_UNI:", ex.getPrice('ETH_UNI'))
    Log("UNI_ETH:", ex.getPrice('UNI_ETH'))

    // ETH 
    Log("balance of ETH", ex.getETHBalance())

    // UNI
    let uniBalance = ex.balanceOf('UNI')
    Log("balance of UNI", uniBalance)

    // LINK
    let linkBalance = ex.balanceOf('LINK')
    Log("balance of LINK", linkBalance)

    // swap 0.001 ETH to UNI
    Log(ex.swapToken('ETH', 0.001, 'UNI'))

    // swap UNI to LINK
    Log(ex.swapToken('UNI', ex.balanceOf('UNI') - uniBalance, 'LINK'))
}

In the test code, we tested the printing of wallet address, registration of token information, printing of asset balance, and a continuous exchange of ETH ->UNI ->LINK. It should be noted that the token address registered here is on the Goorli test network of Ethereum, so the token address with the same name is different. As for the test token, you can use the faucet of this test network to apply for the test token, and the details can be queried by Google.

Note that you must check the "Uniswap V3 Trade Template" to use the $.NewUniswapV3() function. If your FMZ account does not have this template, you can click Get here.

Strategy backtest logs:

Asset values displayed on the Uniswap page

https://app.uniswap.org/

These operations can also be queried in the chain:

https://goerli.etherscan.io/

ETH was converted to UNI once, UNI authorization was executed once, and UNI was exchanged to LINK once.

END

There are many functions of this class library that can be extended, and can even be extended to package multiple exchanges to realize tokenA ->tokenB ->tokenC path exchange. It can be optimized and expanded according to the needs. This kind of library code is mainly for teaching.

Update

The swapToken function has been upgraded to support tokenA ->tokenB ->tokenC... -> TokenD continuous exchange function. You can check the latest code of the template published by Strategy Square on FMZ platform.

From: https://blog.mathquant.com/2023/02/09/elegant-and-simple-accessed-uniswap-v3-on-fmz-with-200-lines-of-code.html

0
Subscribe to my newsletter

Read articles from FMZ Quant directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

FMZ Quant
FMZ Quant

Quantitative Trading For Everyone