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

  1. Overview

  2. Protocol Requirements

  3. Adding a Protocol

  4. Creating Protocol Adapters

  5. Cross-Chain Support

  6. Testing New Protocols

  7. Protocol Management

  8. Best Practices

  9. 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:

  1. Collateralization Mechanism: The protocol must support collateralized debt positions

  2. Liquidation Process: Clear rules for liquidation thresholds and processes

  3. Token Standards: Compatible with ERC20 tokens for collateral and debt

  4. Public Interface: Well-documented public interface for interaction

  5. 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 ID

Step 2: Register the Protocol

Use the addProtocol function to register the new protocol. This can be done:

  1. During initialization: Add to the initializeCollatinatiorStorage function

  2. Post-deployment: Call the addProtocol function 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

  1. Protocol Availability: Not all protocols are available on all chains

  2. Contract Addresses: The same protocol may have different contract addresses on different chains

  3. Gas Costs: Operations may have different gas costs across chains

  4. 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:

  1. Message Passing: Use cross-chain messaging protocols like LayerZero, Axelar, or Wormhole

  2. State Verification: Implement state verification to ensure data integrity across chains

  3. Fallback Mechanisms: Design fallback mechanisms for when cross-chain communication fails

Testing New Protocols

Before deploying to production, thoroughly test the new protocol integration:

  1. Unit Tests: Test each adapter function independently

  2. Integration Tests: Test the full flow from creating to closing positions

  3. Edge Cases: Test with minimum/maximum values and unusual scenarios

  4. 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

  1. Security First: Always prioritize security when integrating with external protocols

  2. Conservative Thresholds: Start with conservative liquidation thresholds and adjust based on protocol behavior

  3. Thorough Testing: Test all edge cases before deploying to production

  4. Monitoring: Implement monitoring for protocol health and position status

  5. Upgradeability: Design adapters to be upgradeable if protocol interfaces change

  6. 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 used

Step 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.