Adding New Protocols to Collatinator
This document provides a comprehensive guide on how to add new CDP (Collateralized Debt Position) protocols to the Collatinator facet in the Diamond Vaultinator framework.
Table of Contents
Overview
Protocol Requirements
Adding a Protocol
Creating Protocol Adapters
Cross-Chain Support
Testing New Protocols
Protocol Management
Best Practices
Example: Aave Protocol Integration
Overview
The Collatinator facet supports multiple CDP protocols like MakerDAO, Aave, Compound, and Liquity. Each protocol has its own rules for collateralization, liquidation thresholds, and other parameters. This guide explains how to extend the Collatinator to support additional protocols.
Protocol Requirements
Before adding a new protocol, ensure it meets these requirements:
Collateralization Mechanism: The protocol must support collateralized debt positions
Liquidation Process: Clear rules for liquidation thresholds and processes
Token Standards: Compatible with ERC20 tokens for collateral and debt
Public Interface: Well-documented public interface for interaction
Chain Compatibility: Supported on the target blockchain network
Adding a Protocol
Step 1: Define Protocol ID
First, add a new protocol constant in the CollatinatiorFacet.sol file:
// Add this with the other protocol constants
uint8 public constant PROTOCOL_NEW_PROTOCOL = 4; // Increment from the last used IDStep 2: Register the Protocol
Use the addProtocol function to register the new protocol. This can be done:
During initialization: Add to the
initializeCollatinatiorStoragefunctionPost-deployment: Call the
addProtocolfunction with admin rights
// Example for adding during initialization
_addProtocol(PROTOCOL_NEW_PROTOCOL, "New Protocol Name", 150, newProtocolAdapter);
// Example for adding post-deployment (must be called by VAULT_MANAGER_ROLE)
function exampleAddingProtocol() external {
// Create and deploy adapter first (see next section)
address adapter = address(newlyDeployedAdapter);
// Then call addProtocol
addProtocol(PROTOCOL_NEW_PROTOCOL, "New Protocol Name", 150, adapter);
}Parameters:
_protocolId: Unique identifier for the protocol (use the constant defined in Step 1)_name: Human-readable name of the protocol_liquidationThreshold: Default liquidation threshold (e.g., 150 means 150% collateralization required)_adapter: Address of the protocol adapter (see next section)
Creating Protocol Adapters
Protocol adapters are contracts that handle the specific interactions with each CDP protocol.
Step 1: Create Adapter Interface
// IProtocolAdapter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IProtocolAdapter {
function createPosition(
address collateralToken,
address debtToken,
uint256 collateralAmount,
uint256 debtAmount
) external returns (bytes memory positionData);
function closePosition(bytes memory positionData) external;
function addCollateral(bytes memory positionData, uint256 amount) external;
function removeCollateral(bytes memory positionData, uint256 amount) external;
function increaseDebt(bytes memory positionData, uint256 amount) external;
function decreaseDebt(bytes memory positionData, uint256 amount) external;
function getHealthFactor(bytes memory positionData) external view returns (uint256);
function canBeLiquidated(bytes memory positionData) external view returns (bool);
}Step 2: Implement Protocol-Specific Adapter
Create a new adapter that implements the IProtocolAdapter interface:
// NewProtocolAdapter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IProtocolAdapter.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract NewProtocolAdapter is IProtocolAdapter {
using SafeERC20 for IERC20;
// Protocol-specific contracts and interfaces
INewProtocol public immutable protocol;
constructor(address _protocol) {
protocol = INewProtocol(_protocol);
}
// Implement all interface functions with protocol-specific logic
function createPosition(
address collateralToken,
address debtToken,
uint256 collateralAmount,
uint256 debtAmount
) external override returns (bytes memory) {
// Protocol-specific implementation
// ...
// Return position data that can be used to identify this position
return abi.encode(/* position identifier data */);
}
// Implement remaining interface functions
// ...
}Cross-Chain Support
The Collatinator facet can support protocols across different blockchain networks. Here's how to handle cross-chain protocol integration:
Chain-Specific Considerations
Protocol Availability: Not all protocols are available on all chains
Contract Addresses: The same protocol may have different contract addresses on different chains
Gas Costs: Operations may have different gas costs across chains
Block Confirmation Times: Consider different block confirmation times when designing liquidation mechanisms
Implementing Chain-Specific Adapters
For protocols that exist on multiple chains, create chain-specific adapters:
// Example: Aave adapter for Ethereum mainnet
contract AaveEthereumAdapter is IProtocolAdapter {
// Ethereum mainnet specific implementation
}
// Example: Aave adapter for Polygon
contract AavePolygonAdapter is IProtocolAdapter {
// Polygon specific implementation
}Chain ID Detection
Use the block.chainid to detect the current chain and apply chain-specific logic:
function getChainSpecificAddress() internal view returns (address) {
if (block.chainid == 1) {
return ethereumAddress; // Ethereum mainnet
} else if (block.chainid == 137) {
return polygonAddress; // Polygon
} else if (block.chainid == 42161) {
return arbitrumAddress; // Arbitrum
} else {
revert("Chain not supported");
}
}Cross-Chain Communication
For protocols that require cross-chain communication:
Message Passing: Use cross-chain messaging protocols like LayerZero, Axelar, or Wormhole
State Verification: Implement state verification to ensure data integrity across chains
Fallback Mechanisms: Design fallback mechanisms for when cross-chain communication fails
Testing New Protocols
Before deploying to production, thoroughly test the new protocol integration:
Unit Tests: Test each adapter function independently
Integration Tests: Test the full flow from creating to closing positions
Edge Cases: Test with minimum/maximum values and unusual scenarios
Gas Optimization: Ensure operations are gas-efficient
Protocol Management
After adding a protocol, you can manage it using these functions:
Updating Liquidation Threshold
// Must be called by VAULT_MANAGER_ROLE
function updateThreshold() external {
uint8 protocolId = PROTOCOL_NEW_PROTOCOL;
uint256 newThreshold = 175; // 175% collateralization required
setLiquidationThreshold(protocolId, newThreshold);
}Removing a Protocol
// Must be called by VAULT_MANAGER_ROLE
function disableProtocol() external {
uint8 protocolId = PROTOCOL_NEW_PROTOCOL;
removeProtocol(protocolId);
}Note: Removing a protocol only prevents new positions from being created. Existing positions remain active.
Best Practices
Security First: Always prioritize security when integrating with external protocols
Conservative Thresholds: Start with conservative liquidation thresholds and adjust based on protocol behavior
Thorough Testing: Test all edge cases before deploying to production
Monitoring: Implement monitoring for protocol health and position status
Upgradeability: Design adapters to be upgradeable if protocol interfaces change
Documentation: Document all protocol-specific behaviors and requirements
Example: Aave Protocol Integration
This section provides a detailed example of integrating Aave V3 as a protocol in the Collatinator facet.
Step 1: Define Aave Protocol ID
// In CollatinatiorFacet.sol
uint8 public constant PROTOCOL_AAVE_V3 = 5; // Assuming 0-4 are already usedStep 2: Create Aave Adapter Interface
// IAaveAdapter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IProtocolAdapter.sol";
interface IAaveAdapter is IProtocolAdapter {
// Aave-specific functions
function getReserveData(address asset) external view returns (
uint256 ltv,
uint256 liquidationThreshold,
uint256 liquidationBonus
);
function getAavePool() external view returns (address);
}Step 3: Implement Aave Adapter
// AaveV3Adapter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IAaveAdapter.sol";
import "@aave/protocol-v3/contracts/interfaces/IPool.sol";
import "@aave/protocol-v3/contracts/interfaces/IPoolAddressesProvider.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract AaveV3Adapter is IAaveAdapter {
using SafeERC20 for IERC20;
// Aave contracts
IPoolAddressesProvider public immutable addressesProvider;
// Position data structure
struct AavePosition {
address user;
address collateralToken;
address debtToken;
uint256 collateralAmount;
uint256 debtAmount;
}
constructor(address _addressesProvider) {
addressesProvider = IPoolAddressesProvider(_addressesProvider);
}
function getAavePool() public view override returns (address) {
return addressesProvider.getPool();
}
function getReserveData(address asset) external view override returns (
uint256 ltv,
uint256 liquidationThreshold,
uint256 liquidationBonus
) {
IPool pool = IPool(getAavePool());
(
, // configuration
, // liquidityIndex
, // currentLiquidityRate
, // variableBorrowIndex
, // currentVariableBorrowRate
, // currentStableBorrowRate
, // lastUpdateTimestamp
, // id
, // aTokenAddress
, // stableDebtTokenAddress
, // variableDebtTokenAddress
, // interestRateStrategyAddress
uint256 baseLTV,
uint256 liquidationThresholdValue,
uint256 liquidationBonusValue,
, // decimals
, // reserve factor
, // is active
, // is frozen
, // is borrowing enabled
, // is stable rate borrowing enabled
, // is paused
, // can be liquidated as collateral
, // can use as collateral
, // can borrow
, // can be eMode collateral
, // can be borrowed in eMode
, // is siloed borrowing
, // is flashloanable
, // is active as collateral
, // is active as borrowing
, // is stable borrowing rate enabled
, // is borrowable in isolation
, // is siloed borrowing
, // is flashloanable
, // is active as collateral
, // is active as borrowing
, // is stable borrowing rate enabled
, // is borrowable in isolation
, // is siloed borrowing
, // is flashloanable
, // is active as collateral
, // is active as borrowing
, // is stable borrowing rate enabled
, // is borrowable in isolation
, // is siloed borrowing
, // is flashloanable
, // is active as collateral
, // is active as borrowing
, // is stable borrowing rate enabled
, // is borrowable in isolation
, // is siloed borrowing
, // is flashloanable
, // is active as collateral
, // is active as borrowing
, // is stable borrowing rate enabled
, // is borrowable in isolation
, // is siloed borrowing
, // is flashloanable
, // is active as collateral
, // is active as borrowing
, // is stable borrowing rate enabled
, // is borrowable in isolation
) = pool.getReserveData(asset);
return (baseLTV, liquidationThresholdValue, liquidationBonusValue);
}
function createPosition(
address collateralToken,
address debtToken,
uint256 collateralAmount,
uint256 debtAmount
) external override returns (bytes memory) {
// Get Aave pool
IPool pool = IPool(getAavePool());
// Transfer collateral from user
IERC20(collateralToken).safeTransferFrom(msg.sender, address(this), collateralAmount);
// Approve Aave to use the collateral
IERC20(collateralToken).approve(address(pool), collateralAmount);
// Supply collateral to Aave
pool.supply(collateralToken, collateralAmount, address(this), 0);
// Enable collateral as collateral
pool.setUserUseReserveAsCollateral(collateralToken, true);
// Borrow if needed
if (debtAmount > 0) {
pool.borrow(debtToken, debtAmount, 2, 0, address(this)); // 2 = variable rate
// Transfer borrowed amount to user
IERC20(debtToken).transfer(msg.sender, debtAmount);
}
// Create and return position data
AavePosition memory position = AavePosition({
user: msg.sender,
collateralToken: collateralToken,
debtToken: debtToken,
collateralAmount: collateralAmount,
debtAmount: debtAmount
});
return abi.encode(position);
}
function closePosition(bytes memory positionData) external override {
AavePosition memory position = abi.decode(positionData, (AavePosition));
IPool pool = IPool(getAavePool());
// Repay debt if any
if (position.debtAmount > 0) {
// Transfer debt tokens from user to repay
IERC20(position.debtToken).safeTransferFrom(
msg.sender,
address(this),
position.debtAmount
);
// Approve Aave to use the debt tokens
IERC20(position.debtToken).approve(address(pool), position.debtAmount);
// Repay the debt
pool.repay(position.debtToken, position.debtAmount, 2, address(this)); // 2 = variable rate
}
// Withdraw collateral
pool.withdraw(position.collateralToken, position.collateralAmount, msg.sender);
}
// Implement other required functions
function addCollateral(bytes memory positionData, uint256 amount) external override {
// Implementation
}
function removeCollateral(bytes memory positionData, uint256 amount) external override {
// Implementation
}
function increaseDebt(bytes memory positionData, uint256 amount) external override {
// Implementation
}
function decreaseDebt(bytes memory positionData, uint256 amount) external override {
// Implementation
}
function getHealthFactor(bytes memory positionData) external view override returns (uint256) {
AavePosition memory position = abi.decode(positionData, (AavePosition));
IPool pool = IPool(getAavePool());
(
uint256 totalCollateralBase,
uint256 totalDebtBase,
uint256 availableBorrowsBase,
uint256 currentLiquidationThreshold,
uint256 ltv,
uint256 healthFactor
) = pool.getUserAccountData(address(this));
return healthFactor;
}
function canBeLiquidated(bytes memory positionData) external view override returns (bool) {
uint256 healthFactor = this.getHealthFactor(positionData);
return healthFactor < 1e18; // Health factor below 1.0
}
}Step 4: Deploy and Register Aave Adapter
// Deploy adapter with the correct Aave addresses provider for the target chain
address aaveAddressesProvider;
if (block.chainid == 1) {
// Ethereum mainnet
aaveAddressesProvider = 0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e;
} else if (block.chainid == 137) {
// Polygon
aaveAddressesProvider = 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb;
} else if (block.chainid == 42161) {
// Arbitrum
aaveAddressesProvider = 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb;
} else {
revert("Chain not supported by Aave V3");
}
// Deploy adapter
AaveV3Adapter aaveAdapter = new AaveV3Adapter(aaveAddressesProvider);
// Register protocol in Collatinator
addProtocol(PROTOCOL_AAVE_V3, "Aave V3", 175, address(aaveAdapter));Step 5: Usage Example
// Create an Aave position with WETH as collateral and USDC as debt
uint256 positionId = collatinatiorFacet.createPosition(
PROTOCOL_AAVE_V3,
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, // WETH on Ethereum
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC on Ethereum
1 ether, // 1 WETH as collateral
1000 * 10**6, // 1000 USDC as debt
0 // Use default liquidation threshold
);This example demonstrates how to integrate Aave V3 with the Collatinator facet, including chain-specific considerations and a complete adapter implementation.