Uniswap Protocol Adapter
The Uniswap adapter enables the Yieldinator Facet to integrate with Uniswap's liquidity pools (both V2 and V3), allowing vaults to earn trading fees and optimize yield through concentrated liquidity positions.
Overview
Uniswap is the largest decentralized exchange by volume, offering two distinct liquidity provision mechanisms:
Uniswap V2: Traditional automated market maker with 50/50 liquidity pools
Uniswap V3: Advanced concentrated liquidity that allows LPs to provide liquidity within specific price ranges
The Uniswap adapter facilitates deposits into both V2 and V3 pools, manages LP token balances for V2, handles NFT positions for V3, and optimizes yield by strategically setting price ranges for concentrated liquidity positions.
Implementation Details
Contract: UniswapAdapter.sol
UniswapAdapter.solThe adapter implements the standard YieldinatorAdapter interface with Uniswap-specific functionality:
contract UniswapAdapter is YieldinatorAdapter {
using SafeERC20 for IERC20;
// Uniswap contracts
IUniswapV2Router02 public v2Router;
IUniswapV2Factory public v2Factory;
INonfungiblePositionManager public positionManager;
IUniswapV3Factory public v3Factory;
// Pool version enum
enum PoolVersion { V2, V3 }
// Pool configuration
struct PoolConfig {
address pair; // V2 pair address or V3 pool address
address token0; // First token in the pair
address token1; // Second token in the pair
uint24 fee; // Fee tier for V3 (0 for V2)
PoolVersion version; // V2 or V3
int24 tickLower; // Lower tick for V3 position (0 for V2)
int24 tickUpper; // Upper tick for V3 position (0 for V2)
bool active; // Whether the pool is active
}
// Position tracking for V3
struct Position {
uint256 tokenId; // NFT token ID for the position
uint128 liquidity; // Current liquidity amount
int24 tickLower; // Position's lower tick
int24 tickUpper; // Position's upper tick
}
// Mapping from token to pool configuration
mapping(address => PoolConfig) public tokenToPool;
// Mapping from token to V3 positions
mapping(address => Position[]) public tokenToPositions;
// Deposited amounts per token
mapping(address => uint256) public depositedAmount;
// Constructor
constructor(
address _admin,
address _v2Router,
address _v2Factory,
address _positionManager,
address _v3Factory
) YieldinatorAdapter("Uniswap", _admin) {
require(_v2Router != address(0), "UniswapAdapter: v2Router cannot be zero address");
require(_v2Factory != address(0), "UniswapAdapter: v2Factory cannot be zero address");
require(_positionManager != address(0), "UniswapAdapter: positionManager cannot be zero address");
require(_v3Factory != address(0), "UniswapAdapter: v3Factory cannot be zero address");
v2Router = IUniswapV2Router02(_v2Router);
v2Factory = IUniswapV2Factory(_v2Factory);
positionManager = INonfungiblePositionManager(_positionManager);
v3Factory = IUniswapV3Factory(_v3Factory);
}
}Key Functions
Pool Registration
Before using the adapter for a specific token, the pool must be registered:
function registerV2Pool(
address _token,
address _pairedToken
) external onlyRole(ADMIN_ROLE) {
require(_token != address(0), "UniswapAdapter: token cannot be zero address");
require(_pairedToken != address(0), "UniswapAdapter: paired token cannot be zero address");
// Get the pair address from the factory
address pair = v2Factory.getPair(_token, _pairedToken);
require(pair != address(0), "UniswapAdapter: pair does not exist");
// Determine token order
(address token0, address token1) = _token < _pairedToken
? (_token, _pairedToken)
: (_pairedToken, _token);
// Create and store pool configuration
PoolConfig memory config = PoolConfig({
pair: pair,
token0: token0,
token1: token1,
fee: 0,
version: PoolVersion.V2,
tickLower: 0,
tickUpper: 0,
active: true
});
tokenToPool[_token] = config;
emit V2PoolRegistered(_token, pair);
}
function registerV3Pool(
address _token,
address _pairedToken,
uint24 _fee,
int24 _tickLower,
int24 _tickUpper
) external onlyRole(ADMIN_ROLE) {
require(_token != address(0), "UniswapAdapter: token cannot be zero address");
require(_pairedToken != address(0), "UniswapAdapter: paired token cannot be zero address");
require(_fee > 0, "UniswapAdapter: fee must be greater than 0");
require(_tickLower < _tickUpper, "UniswapAdapter: tickLower must be less than tickUpper");
// Determine token order
(address token0, address token1) = _token < _pairedToken
? (_token, _pairedToken)
: (_pairedToken, _token);
// Get the pool address from the factory
address pool = v3Factory.getPool(token0, token1, _fee);
require(pool != address(0), "UniswapAdapter: pool does not exist");
// Create and store pool configuration
PoolConfig memory config = PoolConfig({
pair: pool,
token0: token0,
token1: token1,
fee: _fee,
version: PoolVersion.V3,
tickLower: _tickLower,
tickUpper: _tickUpper,
active: true
});
tokenToPool[_token] = config;
emit V3PoolRegistered(_token, pool, _fee, _tickLower, _tickUpper);
}Deposit
The deposit function handles adding liquidity to Uniswap V2 or V3 based on the pool configuration:
function deposit(address _token, uint256 _amount)
external
override
onlyRole(YIELDINATOR_ROLE)
nonReentrant
returns (bool)
{
require(_amount > 0, "UniswapAdapter: amount must be greater than 0");
PoolConfig memory config = tokenToPool[_token];
require(config.active, "UniswapAdapter: pool not active for token");
// Transfer token from caller
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
if (config.version == PoolVersion.V2) {
return _depositV2(_token, _amount, config);
} else {
return _depositV3(_token, _amount, config);
}
}
function _depositV2(address _token, uint256 _amount, PoolConfig memory _config)
internal
returns (bool)
{
// Determine other token in pair and amount needed
address otherToken = (_token == _config.token0) ? _config.token1 : _config.token0;
uint256 otherAmount = _calculateRequiredAmount(_token, otherToken, _amount);
// Approve router to use tokens
IERC20(_token).safeApprove(address(v2Router), _amount);
IERC20(otherToken).safeApprove(address(v2Router), otherAmount);
// Add liquidity to Uniswap V2
(uint256 amountA, uint256 amountB, uint256 liquidity) = v2Router.addLiquidity(
_token,
otherToken,
_amount,
otherAmount,
_amount * 95 / 100, // 5% slippage
otherAmount * 95 / 100, // 5% slippage
address(this),
block.timestamp + 1800 // 30 minutes deadline
);
// Update deposited amount
depositedAmount[_token] += amountA;
emit Deposited(_token, amountA);
return true;
}
function _depositV3(address _token, uint256 _amount, PoolConfig memory _config)
internal
returns (bool)
{
// Determine other token in pair and amount needed
address otherToken = (_token == _config.token0) ? _config.token1 : _config.token0;
uint256 otherAmount = _calculateRequiredAmountV3(_token, otherToken, _amount, _config);
// Approve position manager to use tokens
IERC20(_token).safeApprove(address(positionManager), _amount);
IERC20(otherToken).safeApprove(address(positionManager), otherAmount);
// Prepare mint parameters
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({
token0: _config.token0,
token1: _config.token1,
fee: _config.fee,
tickLower: _config.tickLower,
tickUpper: _config.tickUpper,
amount0Desired: _token == _config.token0 ? _amount : otherAmount,
amount1Desired: _token == _config.token0 ? otherAmount : _amount,
amount0Min: _token == _config.token0 ? _amount * 95 / 100 : otherAmount * 95 / 100,
amount1Min: _token == _config.token0 ? otherAmount * 95 / 100 : _amount * 95 / 100,
recipient: address(this),
deadline: block.timestamp + 1800 // 30 minutes deadline
});
// Mint new position
(uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = positionManager.mint(params);
// Store position information
Position memory position = Position({
tokenId: tokenId,
liquidity: liquidity,
tickLower: _config.tickLower,
tickUpper: _config.tickUpper
});
tokenToPositions[_token].push(position);
// Update deposited amount
uint256 depositedTokenAmount = _token == _config.token0 ? amount0 : amount1;
depositedAmount[_token] += depositedTokenAmount;
emit DepositedV3(_token, depositedTokenAmount, tokenId, liquidity);
return true;
}Withdraw
The withdraw function handles removing liquidity from Uniswap V2 or V3 based on the pool configuration:
function withdraw(address _token, uint256 _amount)
external
override
onlyRole(YIELDINATOR_ROLE)
nonReentrant
returns (uint256)
{
require(_amount > 0, "UniswapAdapter: amount must be greater than 0");
require(_amount <= depositedAmount[_token], "UniswapAdapter: insufficient balance");
PoolConfig memory config = tokenToPool[_token];
require(config.active, "UniswapAdapter: pool not active for token");
if (config.version == PoolVersion.V2) {
return _withdrawV2(_token, _amount, config);
} else {
return _withdrawV3(_token, _amount, config);
}
}
function _withdrawV2(address _token, uint256 _amount, PoolConfig memory _config)
internal
returns (uint256)
{
// Calculate proportion of LP tokens to withdraw
address pair = _config.pair;
uint256 lpBalance = IERC20(pair).balanceOf(address(this));
uint256 lpToWithdraw = lpBalance * _amount / depositedAmount[_token];
// Determine other token in pair
address otherToken = (_token == _config.token0) ? _config.token1 : _config.token0;
// Approve router to use LP tokens
IERC20(pair).safeApprove(address(v2Router), lpToWithdraw);
// Remove liquidity from Uniswap V2
(uint256 amountA, uint256 amountB) = v2Router.removeLiquidity(
_token,
otherToken,
lpToWithdraw,
_amount * 95 / 100, // 5% slippage
0, // Min amount for other token
address(this),
block.timestamp + 1800 // 30 minutes deadline
);
// Update deposited amount
depositedAmount[_token] -= amountA;
// Transfer withdrawn token to caller
IERC20(_token).safeTransfer(msg.sender, amountA);
emit Withdrawn(_token, amountA);
return amountA;
}
function _withdrawV3(address _token, uint256 _amount, PoolConfig memory _config)
internal
returns (uint256)
{
// Find positions to withdraw from
Position[] storage positions = tokenToPositions[_token];
require(positions.length > 0, "UniswapAdapter: no positions found");
uint256 remainingAmount = _amount;
uint256 totalWithdrawn = 0;
// Loop through positions and withdraw until requested amount is reached
for (uint256 i = 0; i < positions.length && remainingAmount > 0; i++) {
Position storage position = positions[i];
// Calculate proportion of liquidity to withdraw from this position
uint128 liquidityToWithdraw = uint128(uint256(position.liquidity) * remainingAmount / depositedAmount[_token]);
if (liquidityToWithdraw > 0) {
// Prepare decrease liquidity parameters
INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: position.tokenId,
liquidity: liquidityToWithdraw,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp + 1800 // 30 minutes deadline
});
// Decrease liquidity
(uint256 amount0, uint256 amount1) = positionManager.decreaseLiquidity(params);
// Prepare collect parameters
INonfungiblePositionManager.CollectParams memory collectParams = INonfungiblePositionManager.CollectParams({
tokenId: position.tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
});
// Collect tokens
(uint256 collected0, uint256 collected1) = positionManager.collect(collectParams);
// Update position liquidity
position.liquidity -= liquidityToWithdraw;
// Calculate withdrawn amount of requested token
uint256 withdrawnAmount = _token == _config.token0 ? collected0 : collected1;
// Update remaining amount to withdraw
if (withdrawnAmount >= remainingAmount) {
totalWithdrawn += remainingAmount;
remainingAmount = 0;
} else {
totalWithdrawn += withdrawnAmount;
remainingAmount -= withdrawnAmount;
}
}
}
// Update deposited amount
depositedAmount[_token] -= totalWithdrawn;
// Transfer withdrawn token to caller
IERC20(_token).safeTransfer(msg.sender, totalWithdrawn);
emit Withdrawn(_token, totalWithdrawn);
return totalWithdrawn;
}Harvest Yield
The harvestYield function collects fees from V2 and V3 positions:
function harvestYield(address _token)
external
override
onlyRole(YIELDINATOR_ROLE)
nonReentrant
returns (uint256)
{
PoolConfig memory config = tokenToPool[_token];
require(config.active, "UniswapAdapter: pool not active for token");
if (config.version == PoolVersion.V2) {
// V2 has no explicit yield harvesting - fees are automatically added to the pool
return 0;
} else {
return _harvestV3Fees(_token, config);
}
}
function _harvestV3Fees(address _token, PoolConfig memory _config)
internal
returns (uint256)
{
Position[] storage positions = tokenToPositions[_token];
uint256 totalHarvested = 0;
for (uint256 i = 0; i < positions.length; i++) {
Position storage position = positions[i];
if (position.liquidity > 0) {
// Prepare collect parameters
INonfungiblePositionManager.CollectParams memory params = INonfungiblePositionManager.CollectParams({
tokenId: position.tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
});
// Collect fees
(uint256 collected0, uint256 collected1) = positionManager.collect(params);
// Calculate harvested amount of requested token
uint256 harvestedAmount = _token == _config.token0 ? collected0 : collected1;
totalHarvested += harvestedAmount;
}
}
// Transfer harvested tokens to caller
if (totalHarvested > 0) {
IERC20(_token).safeTransfer(msg.sender, totalHarvested);
}
emit YieldHarvested(_token, totalHarvested);
return totalHarvested;
}Emergency Withdraw
The emergencyWithdraw function provides a way to recover funds in case of emergencies:
function emergencyWithdraw(address _token)
external
override
onlyRole(YIELDINATOR_ROLE)
nonReentrant
returns (uint256)
{
PoolConfig memory config = tokenToPool[_token];
require(config.active, "UniswapAdapter: pool not active for token");
uint256 withdrawnAmount = 0;
if (config.version == PoolVersion.V2) {
// Emergency withdraw from V2
address pair = config.pair;
uint256 lpBalance = IERC20(pair).balanceOf(address(this));
if (lpBalance > 0) {
// Determine other token in pair
address otherToken = (_token == config.token0) ? config.token1 : config.token0;
// Approve router to use LP tokens
IERC20(pair).safeApprove(address(v2Router), lpBalance);
// Remove all liquidity from Uniswap V2
(uint256 amountA, uint256 amountB) = v2Router.removeLiquidity(
_token,
otherToken,
lpBalance,
0, // Min amount for token
0, // Min amount for other token
address(this),
block.timestamp + 1800 // 30 minutes deadline
);
withdrawnAmount = amountA;
}
} else {
// Emergency withdraw from V3
Position[] storage positions = tokenToPositions[_token];
for (uint256 i = 0; i < positions.length; i++) {
Position storage position = positions[i];
if (position.liquidity > 0) {
// Decrease all liquidity
INonfungiblePositionManager.DecreaseLiquidityParams memory params = INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: position.tokenId,
liquidity: position.liquidity,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp + 1800 // 30 minutes deadline
});
positionManager.decreaseLiquidity(params);
// Collect all tokens
INonfungiblePositionManager.CollectParams memory collectParams = INonfungiblePositionManager.CollectParams({
tokenId: position.tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
});
(uint256 collected0, uint256 collected1) = positionManager.collect(collectParams);
// Add to withdrawn amount
withdrawnAmount += _token == config.token0 ? collected0 : collected1;
// Update position liquidity
position.liquidity = 0;
}
}
}
// Reset deposited amount
depositedAmount[_token] = 0;
// Transfer all tokens to caller
uint256 balance = IERC20(_token).balanceOf(address(this));
if (balance > 0) {
IERC20(_token).safeTransfer(msg.sender, balance);
}
emit EmergencyWithdrawn(_token, balance);
return balance;
}Usage Examples
Registering a Uniswap V2 Pool
// Register a WETH-USDC V2 pool for WETH deposits
uniswapAdapter.registerV2Pool(
WETH_ADDRESS,
USDC_ADDRESS
);Registering a Uniswap V3 Pool with Concentrated Liquidity
// Register a WETH-USDC V3 pool with 0.3% fee and specific price range
// Price range from 1500 USDC to 2000 USDC per ETH
int24 tickLower = -887220; // Corresponds to ~1500 USDC per ETH
int24 tickUpper = -887020; // Corresponds to ~2000 USDC per ETH
uniswapAdapter.registerV3Pool(
WETH_ADDRESS,
USDC_ADDRESS,
3000, // 0.3% fee tier
tickLower,
tickUpper
);Depositing into Uniswap
// Deposit 1 ETH into the registered pool
uint256 amount = 1 ether;
yieldinator.deposit(WETH_ADDRESS, amount, uniswapAdapter);Withdrawing from Uniswap
// Withdraw 0.5 ETH from the registered pool
uint256 amount = 0.5 ether;
yieldinator.withdraw(WETH_ADDRESS, amount, uniswapAdapter);Harvesting Yield
// Harvest trading fees for WETH
yieldinator.harvestYield(WETH_ADDRESS, uniswapAdapter);Security Considerations
Price Impact and Slippage
When depositing or withdrawing, the adapter uses a default slippage tolerance of 5%. This can be adjusted by the protocol administrator based on market conditions.
Concentrated Liquidity Risks
For V3 positions, there's a risk of providing liquidity outside the active trading range, resulting in impermanent loss without earning fees. The adapter mitigates this by:
Allowing administrators to set appropriate tick ranges
Supporting multiple positions with different ranges for the same token
Providing functions to adjust tick ranges as market conditions change
Position Management
V3 positions are represented as NFTs, which the adapter manages internally. The contract includes safeguards to ensure these NFTs cannot be transferred out of the adapter except through the proper withdrawal process.
Oracle Manipulation
The adapter relies on Uniswap's time-weighted average price (TWAP) oracles for certain calculations. To mitigate manipulation risks, the adapter:
Uses multiple oracle observations
Implements circuit breakers for extreme price movements
Allows administrators to pause specific pools during market turbulence
Conclusion
The Uniswap adapter provides comprehensive integration with both Uniswap V2 and V3, allowing the Yieldinator Facet to optimize yield through trading fees and concentrated liquidity strategies. The adapter's flexible design accommodates different risk profiles and market conditions, making it a powerful addition to the Vaultinator Protocol's yield generation capabilities.