Practical Examples and Use Cases
Now that we understand the basics of Uniswap v3 and v4, letās explore some practical examples and use cases. These examples will help you understand how to use Uniswap in real-world scenarios.
1. Creating a Custom Token and Liquidity Pool
One common use case is creating a new token and establishing liquidity for it. Hereās a step-by-step guide:
Step 1: Create an ERC-20 Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
}
}
Step 2: Deploy the Token
// Deploy with initial supply of 1 million tokens
MyToken token = new MyToken(1000000 * 10**18);
Step 3: Create a Uniswap v3 Pool
// Create a pool with WETH, 0.3% fee
// Initial price: 0.01 ETH per token (100 tokens per ETH)
uint160 sqrtPriceX96 = 79228162514264337593543950336; // Approx. for 0.01
address pool = factory.createPool(address(token), WETH, 3000);
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
Step 4: Add Initial Liquidity
// Approve tokens
token.approve(address(positionManager), 100000 * 10**18);
IWETH(WETH).approve(address(positionManager), 1000 * 10**18);
// Add liquidity with price range ±50% around current price
int24 tickLower = -69082; // Approx. 0.005 ETH per token
int24 tickUpper = -62148; // Approx. 0.02 ETH per token
positionManager.mint(INonfungiblePositionManager.MintParams({
token0: address(token) < WETH ? address(token) : WETH,
token1: address(token) < WETH ? WETH : address(token),
fee: 3000,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: address(token) < WETH ? 100000 * 10**18 : 1000 * 10**18,
amount1Desired: address(token) < WETH ? 1000 * 10**18 : 100000 * 10**18,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp + 15 minutes
}));
2. Building a Simple DEX Aggregator
A DEX aggregator checks multiple sources for the best price. Hereās a simplified example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@sushiswap/core/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract SimpleAggregator {
ISwapRouter public uniswapV3Router;
IUniswapV2Router02 public sushiswapRouter;
constructor(address _uniswapV3Router, address _sushiswapRouter) {
uniswapV3Router = ISwapRouter(_uniswapV3Router);
sushiswapRouter = IUniswapV2Router02(_sushiswapRouter);
}
function getBestDeal(
address tokenIn,
address tokenOut,
uint256 amountIn
) external view returns (uint256 bestAmountOut, bool useUniswap) {
// Check Uniswap v3 (using multiple fee tiers)
uint256 uniswapAmount = getUniswapQuote(tokenIn, tokenOut, amountIn);
// Check Sushiswap
uint256 sushiswapAmount = getSushiswapQuote(tokenIn, tokenOut, amountIn);
// Return the better deal
if (uniswapAmount >= sushiswapAmount) {
return (uniswapAmount, true);
} else {
return (sushiswapAmount, false);
}
}
function executeSwap(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOutMin
) external returns (uint256 amountOut) {
// Get the best deal
(uint256 expectedOut, bool useUniswap) = getBestDeal(tokenIn, tokenOut, amountIn);
// Ensure we get at least the minimum amount
require(expectedOut >= amountOutMin, "Insufficient output amount");
// Transfer tokens from user
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
IERC20(tokenIn).approve(useUniswap ? address(uniswapV3Router) : address(sushiswapRouter), amountIn);
// Execute the swap on the better platform
if (useUniswap) {
amountOut = swapOnUniswap(tokenIn, tokenOut, amountIn, amountOutMin);
} else {
amountOut = swapOnSushiswap(tokenIn, tokenOut, amountIn, amountOutMin);
}
// Transfer tokens to user
IERC20(tokenOut).transfer(msg.sender, amountOut);
return amountOut;
}
// Helper functions for quotes and swaps...
}
3. Implementing a Dollar-Cost Averaging (DCA) Strategy
DCA is a popular investment strategy. Hereās how to implement it with Uniswap:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@chainlink/contracts/src/v0.8/AutomationCompatible.sol";
contract DCAStrategy is AutomationCompatible {
ISwapRouter public swapRouter;
address public owner;
address public tokenIn;
address public tokenOut;
uint24 public fee;
uint256 public amountPerPeriod;
uint256 public lastExecutionTime;
uint256 public interval; // in seconds
constructor(
address _swapRouter,
address _tokenIn,
address _tokenOut,
uint24 _fee,
uint256 _amountPerPeriod,
uint256 _interval
) {
swapRouter = ISwapRouter(_swapRouter);
owner = msg.sender;
tokenIn = _tokenIn;
tokenOut = _tokenOut;
fee = _fee;
amountPerPeriod = _amountPerPeriod;
interval = _interval;
lastExecutionTime = block.timestamp;
}
function checkUpkeep(bytes calldata) external view override returns (bool upkeepNeeded, bytes memory) {
upkeepNeeded = (block.timestamp - lastExecutionTime) >= interval;
return (upkeepNeeded, "");
}
function performUpkeep(bytes calldata) external override {
if ((block.timestamp - lastExecutionTime) >= interval) {
lastExecutionTime = block.timestamp;
executeSwap();
}
}
function executeSwap() internal {
uint256 balance = IERC20(tokenIn).balanceOf(address(this));
require(balance >= amountPerPeriod, "Insufficient balance");
IERC20(tokenIn).approve(address(swapRouter), amountPerPeriod);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: fee,
recipient: address(this),
deadline: block.timestamp + 15 minutes,
amountIn: amountPerPeriod,
amountOutMinimum: 0, // No slippage protection for simplicity
sqrtPriceLimitX96: 0
});
swapRouter.exactInputSingle(params);
}
function withdraw(address token, uint256 amount) external {
require(msg.sender == owner, "Only owner");
IERC20(token).transfer(owner, amount);
}
}
4. Creating a Uniswap v4 Hook for Limit Orders
One exciting use case for Uniswap v4 is implementing limit orders directly in the protocol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@uniswap/v4-core/contracts/interfaces/IHooks.sol";
import "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol";
contract LimitOrderHook is IHooks {
struct LimitOrder {
address owner;
bool zeroForOne;
uint256 amountIn;
uint256 amountOut;
uint160 sqrtPriceLimitX96;
}
mapping(bytes32 => LimitOrder[]) public limitOrders;
function placeLimitOrder(
bytes32 poolKey,
bool zeroForOne,
uint256 amountIn,
uint256 amountOut,
uint160 sqrtPriceLimitX96
) external {
// Transfer tokens from user
(address token0, address token1, , ) = IPoolManager(msg.sender).getPoolParameters(poolKey);
address tokenIn = zeroForOne ? token0 : token1;
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
// Store the limit order
limitOrders[poolKey].push(LimitOrder({
owner: msg.sender,
zeroForOne: zeroForOne,
amountIn: amountIn,
amountOut: amountOut,
sqrtPriceLimitX96: sqrtPriceLimitX96
}));
}
function afterSwap(
address,
IPoolManager.PoolKey calldata key,
IPoolManager.SwapParams calldata params
) external returns (bytes4) {
// Get current price
uint160 sqrtPriceX96 = IPoolManager(msg.sender).getSqrtPriceX96(key);
// Check if any limit orders can be executed
LimitOrder[] storage orders = limitOrders[key.toId()];
for (uint256 i = 0; i < orders.length; i++) {
LimitOrder storage order = orders[i];
// Check if the price condition is met
bool canExecute = order.zeroForOne
? sqrtPriceX96 <= order.sqrtPriceLimitX96
: sqrtPriceX96 >= order.sqrtPriceLimitX96;
if (canExecute) {
// Execute the limit order
executeOrder(key, order, i);
}
}
return IHooks.afterSwap.selector;
}
function executeOrder(
IPoolManager.PoolKey calldata key,
LimitOrder storage order,
uint256 index
) internal {
// Execute the swap
(address token0, address token1, , ) = IPoolManager(msg.sender).getPoolParameters(key);
address tokenIn = order.zeroForOne ? token0 : token1;
address tokenOut = order.zeroForOne ? token1 : token0;
IERC20(tokenIn).approve(address(IPoolManager(msg.sender)), order.amountIn);
uint256 amountOut = IPoolManager(msg.sender).swap(
key,
IPoolManager.SwapParams({
zeroForOne: order.zeroForOne,
amountSpecified: int256(order.amountIn),
sqrtPriceLimitX96: order.sqrtPriceLimitX96
}),
""
);
// Transfer tokens to the order owner
IERC20(tokenOut).transfer(order.owner, amountOut);
// Remove the executed order
orders[index] = orders[orders.length - 1];
orders.pop();
}
// Implement other hook functions...
}
5. Building a Price Oracle Using TWAP
Uniswap can be used as a price oracle through its Time-Weighted Average Price (TWAP) functionality:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
contract UniswapV3Oracle {
function consultTWAP(
address pool,
uint32 twapInterval
) public view returns (uint256 price) {
require(twapInterval > 0, "Interval must be > 0");
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = twapInterval; // from (seconds ago)
secondsAgos[1] = 0; // to (now)
(int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 tick = int24(tickCumulativesDelta / int56(uint56(twapInterval)));
// Calculate price from tick
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick);
price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 192);
return price;
}
}
These examples demonstrate the versatility of Uniswap and how it can be integrated into various DeFi applications. In the next section, weāll look at some common challenges and best practices when working with Uniswap.