Polystream
  • Overview
  • Getting Started
    • The Crypto Problem
  • Polystream
    • Polystream Vaults
    • ERC4337 Smart Account Wallets
    • Crypto-Fiat Ramp
    • Rewards
    • AI Agents
    • Foundry
  • Smart Contracts
    • Adapter
    • Core
    • Strategy
    • Polystream Contract Address
  • Project Architecture
    • Sequence Diagram
    • Cranker
  • Developer Contributions
  • Others
    • Socials
    • Feedback
Powered by GitBook
On this page
  • Protocol Adapter Interface
  • Implementing a Protocol Adapter
  • Example Adapter Implementation: AaveAdapter
  • Testing Your Protocol Adapter
  • Examples

Developer Contributions

This documentation explains how to create your own protocol adapter to integrate additional yield sources into the Polystream ecosystem.

PreviousCrankerNextSocials

Last updated 2 months ago

Polystream is designed with a modular architecture that allows for easy integration of new yield-generating protocols. This is accomplished through the IProtocolAdapter interface, enabling developers to add support for any DeFi protocol that provides yield opportunities.


Protocol Adapter Interface

Protocol adapters serve as standardized bridges between Polystream's core functionality and external DeFi protocols. All adapters implement the interface.

interface IProtocolAdapter {
    function supply(address asset, uint256 amount) external returns (uint256);
    function withdraw(address asset, uint256 amount) external returns (uint256);
    function withdrawToUser(address asset, uint256 amount, address user) external returns (uint256);
    function harvest(address asset) external returns (uint256);
    function convertFeeToReward(address asset, uint256 fee) external;
    function getAPY(address asset) external view returns (uint256);
    function getBalance(address asset) external view returns (uint256);
    function getTotalPrincipal(address asset) external view returns (uint256);
    function isAssetSupported(address asset) external view returns (bool);
    function getProtocolName() external view returns (string memory);
}

Implementing a Protocol Adapter

Step 1: Create a new adapter contract

Create a new contract file in the src/adapters directory that implements the IProtocolAdapter interface:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "./interfaces/IProtocolAdapter.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

/**
 * @title YourProtocolAdapter
 * @notice Adapter for interacting with YourProtocol
 */
contract YourProtocolAdapter is IProtocolAdapter, Ownable {
    // Protocol-specific variables and mappings
    
    // Implementation of interface methods
    // ...
}

Step 2: Implement key functions

2.1 Supply Function

The supply function allows users to deposit assets into the external protocol:

/**
 * @dev Supply assets to the protocol
 * @param asset The address of the asset to supply
 * @param amount The amount of the asset to supply
 * @return The actual amount supplied
 */
function supply(address asset, uint256 amount) external override returns (uint256) {
    require(isAssetSupported(asset), "Asset not supported");
    require(amount > 0, "Amount must be greater than 0");
    
    // Get initial balance to verify transfer
    uint256 initialBalance = IERC20(asset).balanceOf(address(this));
    
    // Transfer asset from sender to this contract
    IERC20(asset).transferFrom(msg.sender, address(this), amount);
    
    // Verify the transfer
    uint256 receivedAmount = IERC20(asset).balanceOf(address(this)) - initialBalance;
    
    // Update your tracking variables
    totalPrincipal[asset] += receivedAmount;
    
    // Protocol-specific deposit logic
    // Example: depositToProtocol(asset, receivedAmount);
    
    return receivedAmount;
}

2.2 Withdraw Function

The withdraw function allows users to withdraw assets from the protocol:

/**
 * @dev Withdraw assets from the protocol
 * @param asset The address of the asset to withdraw
 * @param amount The amount of the asset to withdraw
 * @return The actual amount withdrawn
 */
function withdraw(address asset, uint256 amount) external override returns (uint256) {
    require(isAssetSupported(asset), "Asset not supported");
    require(amount > 0, "Amount must be greater than 0");
    
    // Calculate maximum withdrawal amount
    uint256 maxWithdrawal = totalPrincipal[asset];
    uint256 withdrawAmount = amount > maxWithdrawal ? maxWithdrawal : amount;
    
    // Protocol-specific withdrawal logic
    // Example: uint256 withdrawn = withdrawFromProtocol(asset, withdrawAmount);
    uint256 withdrawn = 0; // Replace with actual implementation
    
    // Update state
    if (withdrawn <= totalPrincipal[asset]) {
        totalPrincipal[asset] -= withdrawn;
    } else {
        totalPrincipal[asset] = 0;
    }
    
    // Transfer withdrawn assets to the sender
    IERC20(asset).transfer(msg.sender, withdrawn);
    
    return withdrawn;
}

2.3 Harvest Function

The harvest function collects and compounds accumulated yield:

/**
 * @dev Harvest yield from the protocol
 * @param asset The address of the asset
 * @return The total amount harvested
 */
function harvest(address asset) external override returns (uint256) {
    require(isAssetSupported(asset), "Asset not supported");
    
    // Protocol-specific yield harvesting logic
    // Example:
    // 1. Withdraw all assets
    // 2. Calculate yield as (withdrawn - principal)
    // 3. Claim any reward tokens
    // 4. Potentially swap reward tokens to the asset
    // 5. Redeposit everything
    
    uint256 yieldAmount = 0; // Replace with actual implementation
    
    // Update last harvest timestamp
    lastHarvestTimestamp[asset] = block.timestamp;
    
    return yieldAmount;
}

2.4 APY Calculation Function

The getAPY function provides the current annual percentage yield:

/**
 * @dev Get the current APY for an asset
 * @param asset The address of the asset
 * @return The current APY in basis points (1% = 100)
 */
function getAPY(address asset) external view override returns (uint256) {
    require(isAssetSupported(asset), "Asset not supported");
    
    // Protocol-specific APY calculation logic
    // Example: Query protocol's rate, convert to basis points
    
    uint256 apyBps = 0; // Replace with actual implementation
    return apyBps;
}

Step 3: Implement additional helper functions

Additional functions include:

/**
 * @dev Get the current balance in the protocol
 * @param asset The address of the asset
 * @return The current balance
 */
function getBalance(address asset) external view override returns (uint256) {
    require(isAssetSupported(asset), "Asset not supported");
    
    // Protocol-specific balance retrieval
    uint256 balance = 0; // Replace with actual implementation
    return balance;
}

/**
 * @dev Check if an asset is supported
 * @param asset The address of the asset
 * @return True if the asset is supported
 */
function isAssetSupported(address asset) external view override returns (bool) {
    return supportedAssets[asset];
}

/**
 * @dev Get the name of the protocol
 * @return The protocol name
 */
function getProtocolName() external pure override returns (string memory) {
    return "Your Protocol Name";
}

/**
 * @dev Get total principal amount for this asset
 * @param asset The address of the asset
 * @return The total principal amount
 */
function getTotalPrincipal(address asset) external view override returns (uint256) {
    return totalPrincipal[asset];
}

/**
 * @dev Convert fees to rewards in the protocol
 * @param asset The address of the asset
 * @param fee The amount of fee to convert
 */
function convertFeeToReward(address asset, uint256 fee) external override {
    require(isAssetSupported(asset), "Asset not supported");
    require(fee > 0, "Fee must be greater than 0");
    require(fee <= totalPrincipal[asset], "Fee exceeds total principal");
    
    // Reduce the total principal to convert fee to yield
    totalPrincipal[asset] -= fee;
}

/**
 * @dev Withdraw assets from the protocol directly to user
 * @param asset The address of the asset
 * @param amount The amount to withdraw
 * @param user The recipient address
 * @return The actual amount withdrawn
 */
function withdrawToUser(address asset, uint256 amount, address user) external override returns (uint256) {
    // Implementation similar to withdraw but sends assets to user instead of msg.sender
}

Example Adapter Implementation: AaveAdapter

contract AaveAdapter is IProtocolAdapter, Ownable {
    // Aave Pool contract
    IAavePoolMinimal public immutable pool;

    // Optional contracts for reward token harvesting
    IRewardsController public rewardsController;
    IPriceOracleGetter public priceOracle;
    ISyncSwapRouter public syncSwapRouter;

    // Mapping of asset address to aToken address
    mapping(address => address) public aTokens;

    // Supported assets
    mapping(address => bool) public supportedAssets;

    // Protocol name
    string private constant PROTOCOL_NAME = "Aave V3";

    // Tracking initial deposits for profit calculation
    mapping(address => uint256) private initialDeposits;

    // Last harvest timestamp per asset
    mapping(address => uint256) public lastHarvestTimestamp;

    // Add tracking for total principal per asset
    mapping(address => uint256) public totalPrincipal;
    
    // Additional implementation...
}

Key implementation patterns from the AaveAdapter:

  1. Asset Management: Track both principal deposits and yield separately

  2. Yield Harvesting: Withdraw all assets, calculate yield, claim rewards, and redeposit

  3. Principal Protection: Ensure withdrawals don't exceed the tracked principal

  4. Reward Handling: Claim and potentially swap additional reward tokens

Testing Your Protocol Adapter

Create a test file in the test directory to verify your adapter's functionality:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "../src/adapters/YourProtocolAdapter.sol";

contract YourProtocolAdapterTest is Test {
    YourProtocolAdapter public adapter;
    address public testUser;
    address public testAsset;
    
    function setUp() public {
        // Setup adapter and test environment
        adapter = new YourProtocolAdapter(...constructor params...);
        testUser = address(0x1);
        testAsset = address(0x2);
        
        // Mock necessary functions
        // ...
    }
    
    function testSupply() public {
        // Test logic
        // ...
    }
    
    function testWithdraw() public {
        // Test logic
        // ...
    }
    
    function testHarvest() public {
        // Test logic
        // ...
    }
    
    // Additional tests
    // ...
}

Examples

For more comprehensive examples, refer to the existing adapter implementations:

The provides a comprehensive implementation example for integrating with the Aave protocol:

: Integrates with Aave for lending-based yield

: Integrates with SyncSwap for LP-based yield

IProtocolAdapter.sol
AaveAdapter.sol
AaveAdapter.sol
SyncSwapAdapter.sol