This forum post contains a full scope of work that must be performed in order to integrate Uniswap V3 LP positions as collateral for Compound V3 protocol.
Solution
Whitelist
The asset address that will be whitelisted by the governance is represented by the pool address. Each pair in the pool can have up to 4 tiers but usually there are no more than 1 tier per pair depending on its volatility.
Example:
- USDC/USDT has only 0.01% fee tier
- ETH/LINK has only 0.3% fee tier
So usually we’ll maintain only 1(maximum 2) pools per pair.
Supply
We can change the supply method to use ERC721 token ID instead of ERC20 value and asset address.
function supplyCollateral(
address from,
address dst,
uint256 tokenId
) internal {
In order to get Uniswap V3 pool Address we can use a method from the Uniswap V3 libraries.
function computeAddress(
address token0,
address token1,
uint24 fee
) public view returns (address pool) {
pool = address(
uint160(
uint256(
keccak256(
abi.encodePacked(
hex"ff",
factory,
keccak256(
abi.encode(token0, token1, fee)
),
POOL_INIT_CODE_HASH
)
)
)
)
);
}
Then we may use the Uniswap V3 position manager to retrieve the arguments that will help us to identify pool addresses. Next step we may store token id and for pool address both in user positions array and global positions array.
INonfungiblePositionManager.Position
memory _position = INonfungiblePositionManager(positionManager)
.positions(tokenId);
address poolAddress = computeAddress(_position.token0, _position.token1,
_position.fee);
userCollateral[dst][poolAddress].push(tokenId);
tokenIds[poolAddress].push(tokenId);
We will need the second array to easily iterate all the positions by exact pool in order to calculate total supply. So here we’ll have several approaches to calculate the total supply. The main issue here is that we can not summarize ERC721 positions of users without conversion of it to the single ERC20 address but while price of ERC721 position constantly changes we must recalculate all the pool liquidity on each supply. The more users NFTs are there in the pool - the more iterations we have to do, the more expensive gas price of each supply or absorption method becomes.
for (uint256 i = 0; i < tokenIds[assetInfo.asset].length; i++) {
INonfungiblePositionManager.Position
memory _position = INonfungiblePositionManager(
positionManager
).positions(tokenIds[assetInfo.asset][i]);
totalLiquidity += getLiquidity(_position, assetInfo);
}
Total supply recalculation
So the basic approach here is to loop the tokenIds mapping by pool address, extract underlyings, convert them to the single asset using two chainlink endpoints.
(uint256 amount0, uint256 amount1) = getAmountsForLiquidity(_position);
uint256 priceAssetA = getPrice(assetInfo.priceFeed);
uint256 priceAssetB = getPrice(assetInfo.priceFeed);
uint256 amountA = mulPrice(amount0, priceAssetA, assetInfo.scaleA);
uint256 amountB = mulPrice(amount1, priceAssetB, assetInfo.scaleB);
return amountA + amountB;
But it leads to high gas usage as described above because in order to split a token on underlying Uniswap requires square root operations that are quite expensive. Here is the chart that represents how gas costs increase for supply function from 0 to 50 NFTs in the pool.
So in order to prevent constant recalculation of the total supply using supply or absorb method we have an alternative idea.
Offchain service
The external offchain is the most straight-forward solution here if we could use some kind of workers that could execute a recalculation function using time based operations. In order to have the most decentralized solution for such a kind of service we could rely on Chainlink Automation that uses its own network to set up workers. It uses LINK as a gas token and supports lots of networks. The only issue here is that those operations could influence the profit of the protocol while we’ll still have to perform all the calculations on smart contracts. So Chainlink Automation provides a good decentralized solution for the automation workers but using their own offchain service is the most cost-efficient solution with the lack of decentralization. However we could implement this service by ourselves while maintaining the same approach.
So how does it work and why is this approach more preferable than utilizing a common cron/event subscription to maintain workers?
The basic structure of any Chainlink Automation contract must include such functions.
function checkUpkeep(Log calldata log, bytes memory) external view returns (bool upkeepNeeded, bytes memory performData)
{
// checkUpkeep is simulated off chain by a Chainlink DON using OCR3
// Do all of your off computation here for free
// Pass the result of the computation to perfomUpkeep through performData
return (true, performData)
}
function performUpkeep(bytes calldata performData) external
{
//On chain transaction logic
}
Where checkUnkeep function will include the total supply recalculation logic and performUnkeep will be used to set the received value. How does it work? checkUnkeep could be triggered by cron/event subscription using Chainlink DON. It is a fully validatable network of Chainlink Nodes that will call checkUnkeep according to the trigger rules and the received data could be passed to performUnkeep that will call the Comet in order to set the new value. That means that recalculation function will become totally FREE while the protocol only has to pay for setter functions that is much cheaper than maintaining recalculation logic in write methods of smart contracts while preserving the decentralization. The recalculation logic still remains on-chain while the worker’s job is handled by the network of nodes that is much more reliable and stable than building own backend service.
Note: Chainlink keeper contract has to be whitelisted by Compound protocol to be able to perform the update operation.
So that’s why the implementation of Chainlink Automation resolves the problem with gas cost of total supply evaluation while preserving decentralization.
We had a great call with Chainlink Automation architect that answered all our questions and make us comfortable with the solution.
Rewards
One of the remaining questions is - how to deal with the rewards? The rewards in Uniswap V3 position must boost its price but the rewards in Uniswap V3 are not auto recompounded. Moreover there is no view method that allows checking current rewards. Uniswap V3 uses the collect method…
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
function collect( CollectParams calldata params ) external returns (uint256 amount0, uint256 amount1);
…that returns current rewards after actually collecting them. So it creates several boundaries. Here are the two scenarios on how to resolve this bottleneck.
Put it on absorber - Option 1
We can make an absorber to collect the rewards and recompound them into the same position during liquidation. We can provide additional liquidity into the position using increaseLiquidity function on Uniswap V3 position manager which interface is specified below.
struct IncreaseLiquidityParams {
uint256 tokenId;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function increaseLiquidity(
IncreaseLiquidityParams calldata params
)
external
payable
returns (uint128 liquidity, uint256 amount0, uint256 amount1)
This could easily resolve the problem but will lead to higher transaction costs for liquidators.
Rebalance control - Option 2
We can allow users to interact with collateralized LP positions by rebalancing rewards. But if there are any rewards on position during absorption they will not be counted. Rebalancing in terms of Uniswap V3 is not performed in the same position. Instead we have to drain liquidity from the previous position and create a new one with an updated tick range. The main pitfall here is that different tick ranges will require different proportions of underlyings that must be deposited. So in this case rebalance could lead to underlyings remains that will not be redeposited after rebalance. So here we either have to swap the received underlyings in correct proportions or deal with the remains by returning them to the user that will lead to the collateral price decreace.
Absorb and Buy Collateral
In this case we have to also loop the positions but only user’s ones to check if total users pool liquidity is enough to not be liquidated. So limiting maximum users positions will be a good solution in this case to prevent gas overflows. This approach could be used both in isLiquidatable and isBorrowCollateralized.
for ( uint256 i = 0; i < userCollateral[dst][assetInfo.asset].length; i++ ) {
INonfungiblePositionManager.Position memory _position =
INonfungiblePositionManager(positionManager
).positions(userCollateral[dst][assetInfo.asset][i]);
totalLiquidity += getLiquidity(_position, assetInfo);
}
In common Comet we used to provide some amount of base assets to buy the liquidated users position. But now while all the liquidated LP positions are stored as ERC721 assets we could just recalculate the price of the position and discount it, accepting base asset equivalent.
So the buyCollateral interface could be simplified to…
function buyCollateral(
uint256 tokenId,
address recipient
) external nonReentrant;
…as we only need a token ID. So the exact amount accepted by this function and the pool address will be revealed in the function body.
Bulker
We must also have a separate bulker with the interface of LP Comet for supplying ERC721.
function supplyTo(address comet, address to, uint256 tokenId) internal {
CometInterface(comet).supplyFrom(msg.sender, to, tokenId);
}
Moreover it could be used to provide another supply option. Instead of creating a position on Uniswap V3 and supplying in 2 distinct transactions we could provide users a possibility to transfer underlying proportions to create the position on Comet side and supply it immediately.
function supplyTo(address comet, address to, address poolAddress, uint256 amount0, uint256 amount1) internal {
uint256 tokenId = // position creation logic here ;
CometInterface(comet).supplyFrom(msg.sender, to, tokenId);
}
Estimate
Comet Uniswap V3 Integration
Modify the Compound V3 (Comet) protocol to support Uniswap V3 LP tokens as collateral. Handle ERC721 token management, pool address computation, and liquidity tracking.
Subtasks:
- Implement support for ERC721 tokens in the supply logic.
- Develop rewards collection, liquidation, and absorption scenarios for Uniswap V3 LP tokens.
- Integrate Chainlink Automation into Comet infrastructure.
- Update Bulker contract to support Comet LP interface.
Integration of the Chainlink Automation for Supply Recalculation
Develop and integrate an aggregator smart contract and connect it with Chainlink Automation DON to handle total supply recalculations periodically and by event subscriptions, reducing on-chain computation costs. Use or a custom off-chain service for decentralized periodic recalculations.
Subtasks
- Develop a keeper smart contract that computes total supply and allows to set new values on Comet.
- Set up Chainlink Automation and integrate with the keeper smart contract to periodically update the supply.
- Whitelist the keeper contract on LP Comet.
Unit Tests
Write comprehensive unit tests for all new features and modifications. Focus on gas efficiency, edge cases, and protocol-level correctness.
Subtasks:
- Test ERC721 supply and withdrawal.
- Test total supply recalculation logic (on-chain and off-chain).
- Test rewards collection, liquidation, and absorption scenarios.
- Gas cost benchmarking and optimization tests.
Migrations
Perform migrations to deploy updated smart contracts and configure integrations on the protocol.
Subtasks:
- Write scripts to deploy and configure updated contracts.
- Handle migrations for user collateral and positions to the new ERC721-compatible structure.
- Test migrations on a forked mainnet to ensure reliability.
Liquidator
As Comet’s new implementation will be different from the liquidation perspective, we should create a SC that liquidates the position, as well as Typescript logic, that triggers the liquidation.
Subtasks:
- Develop OnChainLPLiquidator Smart contract
- Develop script, that collects positions and trigger the liquidation
Frontend Integration
As WOOF! Handles the development of new Compound Frontend, we need to add the functionality to supply LPs.
Subtasks:
- Update supply collateral flow
- Update other frontend parts to work with LP
Migrations
The development of the whole functionality, including smart contract, liquidator and frontend integration is 1.5 to 2 months.
Next steps
- Community feedback
- Choose the option for rewards