Maple Protocol Adapter

The Maple adapter enables the Yieldinator Facet to integrate with Maple Finance's undercollateralized lending protocol, allowing vaults to earn fixed-income yields by providing liquidity to Maple's lending pools.

Overview

Maple Finance is a decentralized corporate credit market that connects institutional borrowers with lenders through a DAO-managed infrastructure. The Maple adapter facilitates deposits into Maple's lending pools, manages liquidity positions, and harvests yield from loan interest payments and MPL rewards.

Implementation Details

Contract: MapleAdapter.sol

The adapter implements the standard YieldinatorAdapter interface with Maple-specific functionality:

contract MapleAdapter is YieldinatorAdapter {
    using SafeERC20 for IERC20;
    
    // Maple contracts
    address public mplToken; // Maple governance token
    address public xMPL; // Staked MPL token
    IMapleStaking public mapleStaking; // MPL staking contract
    
    // Pool configuration
    struct PoolConfig {
        address pool; // Maple pool address
        address poolToken; // Pool token (LP token)
        bool stakeMPL; // Whether to stake MPL rewards
        bool active; // Whether the pool is active
    }
    
    // Mapping from token to pool configuration
    mapping(address => PoolConfig) public tokenToPool;
    
    // Deposited amounts per token
    mapping(address => uint256) public depositedAmount;
    
    // Constructor
    constructor(
        address _admin,
        address _mplToken,
        address _xMPL,
        address _mapleStaking
    ) YieldinatorAdapter("Maple Finance", _admin) {
        require(_mplToken != address(0), "MapleAdapter: MPL token cannot be zero address");
        require(_xMPL != address(0), "MapleAdapter: xMPL token cannot be zero address");
        require(_mapleStaking != address(0), "MapleAdapter: staking contract cannot be zero address");
        
        mplToken = _mplToken;
        xMPL = _xMPL;
        mapleStaking = IMapleStaking(_mapleStaking);
    }
}

Key Functions

Pool Registration

Before using the adapter for a specific token, the pool must be registered:

function registerPool(
    address _token,
    address _pool,
    address _poolToken,
    bool _stakeMPL
) external onlyRole(ADMIN_ROLE) {
    require(_token != address(0), "MapleAdapter: token cannot be zero address");
    require(_pool != address(0), "MapleAdapter: pool cannot be zero address");
    require(_poolToken != address(0), "MapleAdapter: pool token cannot be zero address");
    
    // Create and store pool configuration
    PoolConfig memory config = PoolConfig({
        pool: _pool,
        poolToken: _poolToken,
        stakeMPL: _stakeMPL,
        active: true
    });
    
    tokenToPool[_token] = config;
    
    emit PoolRegistered(_token, _pool);
}

Deposit

The deposit function handles depositing assets into Maple's lending pools:

function deposit(address _token, uint256 _amount)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (bool)
{
    require(_amount > 0, "MapleAdapter: amount must be greater than 0");
    PoolConfig memory config = tokenToPool[_token];
    require(config.active, "MapleAdapter: pool not active for token");
    
    // Transfer token from caller
    IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
    
    // Approve pool to use tokens
    IERC20(_token).safeApprove(config.pool, _amount);
    
    // Deposit into Maple pool
    uint256 balanceBefore = IERC20(config.poolToken).balanceOf(address(this));
    IMaplePool(config.pool).deposit(_amount);
    uint256 balanceAfter = IERC20(config.poolToken).balanceOf(address(this));
    
    // Calculate pool tokens received
    uint256 poolTokensReceived = balanceAfter - balanceBefore;
    require(poolTokensReceived > 0, "MapleAdapter: no pool tokens received");
    
    // Update deposited amount
    depositedAmount[_token] += _amount;
    
    emit Deposited(_token, _amount);
    return true;
}

Withdraw

The withdraw function handles withdrawing assets from Maple's lending pools:

function withdraw(address _token, uint256 _amount)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    require(_amount > 0, "MapleAdapter: amount must be greater than 0");
    require(_amount <= depositedAmount[_token], "MapleAdapter: insufficient balance");
    
    PoolConfig memory config = tokenToPool[_token];
    require(config.active, "MapleAdapter: pool not active for token");
    
    // Calculate proportion of pool tokens to redeem
    uint256 poolTokenBalance = IERC20(config.poolToken).balanceOf(address(this));
    uint256 poolTokensToRedeem = poolTokenBalance * _amount / depositedAmount[_token];
    
    // Approve pool to use pool tokens
    IERC20(config.poolToken).safeApprove(config.pool, poolTokensToRedeem);
    
    // Withdraw from Maple pool
    uint256 balanceBefore = IERC20(_token).balanceOf(address(this));
    IMaplePool(config.pool).withdraw(poolTokensToRedeem);
    uint256 balanceAfter = IERC20(_token).balanceOf(address(this));
    
    // Calculate amount received
    uint256 amountReceived = balanceAfter - balanceBefore;
    require(amountReceived > 0, "MapleAdapter: no tokens received");
    
    // Transfer withdrawn tokens to caller
    IERC20(_token).safeTransfer(msg.sender, amountReceived);
    
    // Update deposited amount
    depositedAmount[_token] -= _amount;
    
    emit Withdrawn(_token, amountReceived);
    return amountReceived;
}

Harvest Yield

The harvestYield function collects MPL rewards and optionally stakes them:

function harvestYield(address _token)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    PoolConfig memory config = tokenToPool[_token];
    require(config.active, "MapleAdapter: pool not active for token");
    
    // Claim MPL rewards from the pool
    IMaplePool(config.pool).claim();
    
    return _harvestMPLRewards(config);
}

function _harvestMPLRewards(PoolConfig memory config) internal returns (uint256) {
    uint256 mplBalance = IERC20(mplToken).balanceOf(address(this));
    
    // If staking MPL rewards is enabled, stake in Maple staking contract
    if (config.stakeMPL && mplBalance > 0) {
        IERC20(mplToken).safeApprove(address(mapleStaking), mplBalance);
        mapleStaking.stake(mplBalance);
        
        // Calculate xMPL received
        uint256 xMPLBalance = IERC20(xMPL).balanceOf(address(this));
        emit YieldHarvested(mplToken, mplBalance, xMPL, xMPLBalance);
        return xMPLBalance;
    } else if (mplBalance > 0) {
        // Transfer MPL rewards to caller
        IERC20(mplToken).safeTransfer(msg.sender, mplBalance);
        emit YieldHarvested(mplToken, mplBalance, address(0), 0);
        return mplBalance;
    }
    
    return 0;
}

Emergency Withdraw

The emergencyWithdraw function provides a safety mechanism to withdraw all funds:

function emergencyWithdraw(address _token)
    external
    override
    onlyRole(ADMIN_ROLE)
    nonReentrant
    returns (uint256)
{
    PoolConfig memory config = tokenToPool[_token];
    require(config.active, "MapleAdapter: pool not active for token");
    
    // Withdraw all pool tokens from Maple pool
    uint256 poolTokenBalance = IERC20(config.poolToken).balanceOf(address(this));
    if (poolTokenBalance > 0) {
        IERC20(config.poolToken).safeApprove(config.pool, poolTokenBalance);
        IMaplePool(config.pool).withdraw(poolTokenBalance);
    }
    
    // Transfer all token balance to caller
    uint256 tokenBalance = IERC20(_token).balanceOf(address(this));
    if (tokenBalance > 0) {
        IERC20(_token).safeTransfer(msg.sender, tokenBalance);
    }
    
    // Transfer all MPL and xMPL to caller
    uint256 mplBalance = IERC20(mplToken).balanceOf(address(this));
    if (mplBalance > 0) {
        IERC20(mplToken).safeTransfer(msg.sender, mplBalance);
    }
    
    uint256 xMPLBalance = IERC20(xMPL).balanceOf(address(this));
    if (xMPLBalance > 0) {
        IERC20(xMPL).safeTransfer(msg.sender, xMPLBalance);
    }
    
    // Reset deposited amount
    depositedAmount[_token] = 0;
    
    emit EmergencyWithdrawn(_token, tokenBalance);
    return tokenBalance;
}

APY Calculation

The getCurrentAPY function calculates the current APY for a token in the Maple pool:

function getCurrentAPY(address _token)
    external
    view
    override
    returns (uint256)
{
    PoolConfig memory config = tokenToPool[_token];
    if (!config.active) return 0;
    
    // Get base APY from Maple pool
    uint256 baseAPY = IMaplePool(config.pool).getCurrentInterestRate();
    
    // Calculate MPL rewards APY
    uint256 mplRewardsAPY = _calculateMPLRewardsAPY(config.pool);
    
    // Add additional APY from staking MPL if enabled
    uint256 stakingAPY = config.stakeMPL ? _calculateStakingAPY() : 0;
    
    // Combine APYs
    return baseAPY + mplRewardsAPY + stakingAPY;
}

function _calculateMPLRewardsAPY(address _pool) internal view returns (uint256) {
    // Calculate MPL rewards APY based on pool's reward rate and total value locked
    // Implementation depends on Maple's specific reward distribution mechanism
    return IMaplePool(_pool).getRewardsAPY();
}

function _calculateStakingAPY() internal view returns (uint256) {
    // Calculate staking APY based on xMPL growth rate
    return mapleStaking.getStakingAPY();
}

Get Total Deposited

The getTotalDeposited function returns the total amount of tokens deposited:

function getTotalDeposited(address _token)
    external
    view
    override
    returns (uint256)
{
    return depositedAmount[_token];
}

Usage Example

Here's an example of how to use the Maple adapter with the Yieldinator Facet:

// Register the Maple adapter with the Yieldinator Facet
address mapleAdapter = address(new MapleAdapter(
    admin,
    mplToken,
    xMPL,
    mapleStaking
));
yieldinatorFacet.registerAdapter("Maple Finance", mapleAdapter);

// Register a pool for USDC
MapleAdapter(mapleAdapter).registerPool(
    USDC,
    USDC_POOL,
    USDC_POOL_TOKEN,
    true // Stake MPL rewards
);

// Add a yield strategy using the Maple adapter
yieldinatorFacet.addYieldStrategy(
    "Maple USDC Pool",
    mapleAdapter,
    USDC,
    3, // Medium-high risk level (undercollateralized lending)
    1200 // 12.00% APY
);

Security Considerations

  • Credit Risk: Maple Finance pools involve undercollateralized lending to institutional borrowers, which carries inherent credit risk.

  • Liquidity Risk: Withdrawals from Maple pools are subject to liquidity availability and may be delayed if the pool has insufficient funds.

  • Pool Delegate Risk: Maple pools are managed by Pool Delegates who make lending decisions, introducing potential governance risks.

  • Smart Contract Risk: The adapter interacts with multiple external contracts which may have vulnerabilities.

Risk Mitigation

  • The adapter implements strict access controls to prevent unauthorized access.

  • Emergency withdrawal functionality is available to recover funds in case of critical issues.

  • The adapter validates all inputs and handles edge cases to prevent unexpected behavior.

  • Integration with Maple's risk assessment framework to evaluate borrower creditworthiness.

Gas Optimization

  • The adapter minimizes gas usage by batching operations when possible.

  • For harvesting rewards, users can choose whether to stake MPL rewards based on gas costs and expected returns.

  • The adapter avoids unnecessary approvals by only approving tokens when needed.

Future Improvements

  • Support for Maple's credit default swaps (CDS) to hedge against default risk

  • Integration with Maple's governance system for participating in protocol decisions

  • Support for multiple pool delegates to diversify lending risk

  • Automated reinvestment of interest payments for compounding returns