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
The adapter implements the standard YieldinatorAdapter interface with Uniswap-specific functionality:
contractUniswapAdapterisYieldinatorAdapter{usingSafeERC20forIERC20;// Uniswap contracts IUniswapV2Router02 public v2Router; IUniswapV2Factory public v2Factory; INonfungiblePositionManager public positionManager; IUniswapV3Factory public v3Factory;// Pool version enumenumPoolVersion{V2,V3}// Pool configurationstructPoolConfig{address pair;// V2 pair address or V3 pool addressaddress token0;// First token in the pairaddress token1;// Second token in the pairuint24 fee;// Fee tier for V3 (0 for V2) PoolVersion version;// V2 or V3int24 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 V3structPosition{uint256 tokenId;// NFT token ID for the positionuint128 liquidity;// Current liquidity amountint24 tickLower;// Position's lower tickint24 tickUpper;// Position's upper tick}// Mapping from token to pool configurationmapping(address=> PoolConfig)public tokenToPool;// Mapping from token to V3 positionsmapping(address=> Position[])public tokenToPositions;// Deposited amounts per tokenmapping(address=>uint256)public depositedAmount;// Constructorconstructor(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:
Deposit
The deposit function handles adding liquidity to Uniswap V2 or V3 based on the pool configuration:
Withdraw
The withdraw function handles removing liquidity from Uniswap V2 or V3 based on the pool configuration:
Harvest Yield
The harvestYield function collects fees from V2 and V3 positions:
Emergency Withdraw
The emergencyWithdraw function provides a way to recover funds in case of emergencies:
Usage Examples
Registering a Uniswap V2 Pool
Registering a Uniswap V3 Pool with Concentrated Liquidity
Depositing into Uniswap
Withdrawing from Uniswap
Harvesting Yield
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.
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;
}
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;
}
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;
}
// Register a WETH-USDC V2 pool for WETH deposits
uniswapAdapter.registerV2Pool(
WETH_ADDRESS,
USDC_ADDRESS
);
// 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
);
// Deposit 1 ETH into the registered pool
uint256 amount = 1 ether;
yieldinator.deposit(WETH_ADDRESS, amount, uniswapAdapter);
// Withdraw 0.5 ETH from the registered pool
uint256 amount = 0.5 ether;
yieldinator.withdraw(WETH_ADDRESS, amount, uniswapAdapter);
// Harvest trading fees for WETH
yieldinator.harvestYield(WETH_ADDRESS, uniswapAdapter);