From 2fadaa298783817366858ad1e6a75279e296c74e Mon Sep 17 00:00:00 2001 From: Anton Buenavista Date: Tue, 12 Oct 2021 04:12:53 +0800 Subject: [PATCH] Add Pendle Finance adapter --- README.md | 1 + .../adapters/pendle/PendleMarketAdapter.sol | 42 ++++++ .../pendle/PendleMarketTokenAdapter.sol | 135 ++++++++++++++++++ .../adapters/pendle/PendleStakingAdapter.sol | 53 +++++++ test/PendleMarketAdapter.js | 99 +++++++++++++ test/PendleStakingAdapter.js | 68 +++++++++ 6 files changed, 398 insertions(+) create mode 100755 contracts/adapters/pendle/PendleMarketAdapter.sol create mode 100644 contracts/adapters/pendle/PendleMarketTokenAdapter.sol create mode 100644 contracts/adapters/pendle/PendleStakingAdapter.sol create mode 100755 test/PendleMarketAdapter.js create mode 100644 test/PendleStakingAdapter.js diff --git a/README.md b/README.md index 5e686096..5c6a7577 100755 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ All the deployed contracts' addresses are available [here](../../wiki/Addresses) | [Melon](contracts/adapters/melon) | A protocol for decentralized on-chain asset management. | [Asset adapter](contracts/adapters/melon/MelonAssetAdapter.sol) | ["MelonToken"](./contracts/adapters/melon/MelonTokenAdapter.sol) | | [mStable](./contracts/adapters/mstable) | mStable unifies stablecoins, lending and swapping into one standard. | [Asset adapter](./contracts/adapters/mstable/MstableAssetAdapter.sol)
[Staking adapter](./contracts/adapters/mstable/MstableStakingAdapter.sol) | ["Masset"](./contracts/adapters/mstable/MstableTokenAdapter.sol) | | [Nexus Mutual](./contracts/adapters/nexus) | A people-powered alternative to insurance. | [Staking adapter](./contracts/adapters/nexus/NexusStakingAdapter.sol) | — | +| [Pendle Finance](./contracts/adapters/pendle) | Trading future yield. | [Asset adapter](./contracts/adapters/pendle/PendleMarketAdapter.sol)
[Staking adapter](./contracts/adapters/pendle/PendleStakingAdapter.sol) | ["Pendle Market"](./contracts/adapters/pendle/PendleMarketTokenAdapter.sol) | | [Pickle Finance](./contracts/adapters/pickle) | Off peg bad, on peg good. | [Asset adapter](./contracts/adapters/pickle/PickleAssetAdapter.sol)
[Staking adapter V1](./contracts/adapters/pickle/PickleStakingV1Adapter.sol)
[Staking adapter V2](./contracts/adapters/pickle/PickleStakingV2Adapter.sol) | ["PickleJar"](./contracts/adapters/pickle/PickleTokenAdapter.sol) | | [PieDAO](./contracts/adapters/pieDAO) | The Asset Allocation DAO. | [Asset adapter](./contracts/adapters/pieDAO/PieDAOPieAdapter.sol)
[Staking adapter](./contracts/adapters/pieDAO/PieDAOStakingAdapter.sol) | ["PieDAO Pie Token"](./contracts/adapters/pieDAO/PieDAOPieTokenAdapter.sol) | | [PoolTogether](./contracts/adapters/poolTogether) | Decentralized no-loss lottery. Supports SAI, DAI, and USDC pools. | [Asset adapter](./contracts/adapters/poolTogether/PoolTogetherAdapter.sol) | ["PoolTogether pool"](./contracts/adapters/poolTogether/PoolTogetherTokenAdapter.sol) | diff --git a/contracts/adapters/pendle/PendleMarketAdapter.sol b/contracts/adapters/pendle/PendleMarketAdapter.sol new file mode 100755 index 00000000..cddf93b1 --- /dev/null +++ b/contracts/adapters/pendle/PendleMarketAdapter.sol @@ -0,0 +1,42 @@ +// Copyright (C) 2020 Zerion Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.5; +pragma experimental ABIEncoderV2; + +import { ERC20 } from "../../ERC20.sol"; +import { ProtocolAdapter } from "../ProtocolAdapter.sol"; + +/** + * @title Asset adapter for Pendle Finance (markets). + * @dev Implementation of ProtocolAdapter abstract contract. + * @author Anton Buenavista + */ +contract PendleMarketAdapter is ProtocolAdapter { + + string public constant override adapterType = "Asset"; + + string public constant override tokenType = "PendleMarket LP token"; + + /** + * @return Amount of Pendle Market LP tokens held by the given account. + * @param token Address of the Pendle Market. + * @param account Address of the user. + * @dev Implementation of ProtocolAdapter interface function. + */ + function getBalance(address token, address account) public view override returns (uint256) { + return ERC20(token).balanceOf(account); + } +} diff --git a/contracts/adapters/pendle/PendleMarketTokenAdapter.sol b/contracts/adapters/pendle/PendleMarketTokenAdapter.sol new file mode 100644 index 00000000..bb40f8de --- /dev/null +++ b/contracts/adapters/pendle/PendleMarketTokenAdapter.sol @@ -0,0 +1,135 @@ +// Copyright (C) 2020 Zerion Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.5; +pragma experimental ABIEncoderV2; + +import { ERC20 } from "../../ERC20.sol"; +import { TokenMetadata, Component } from "../../Structs.sol"; +import { TokenAdapter } from "../TokenAdapter.sol"; + + +/** + * @dev PendleMarket contract interface. + * Only the functions required for PendleTokenAdapter contract are added. + * The PendleMarket contract is available here + * github.com/pendle-finance/pendle-core/blob/master/contracts/interfaces/IPendleMarket.sol. + */ +interface PendleMarket { + function xyt() external view returns (address); + function token() external view returns (address); + function getReserves() + external + view + returns ( + uint256 xytBalance, + uint256 xytWeight, + uint256 tokenBalance, + uint256 tokenWeight, + uint256 currentBlock + ); +} + +/** + * @title Token adapter for PendleMarket Tokens. + * @dev Implementation of TokenAdapter abstract contract. + * @author Anton Buenavista + */ +contract PendleMarketTokenAdapter is TokenAdapter { + + /** + * @return TokenMetadata struct with ERC20-style token info. + * @dev Implementation of TokenAdapter interface function. + */ + function getMetadata(address token) external view override returns (TokenMetadata memory) { + return TokenMetadata({ + token: token, + name: getMarketName(token), + symbol: ERC20(token).symbol(), + decimals: ERC20(token).decimals() + }); + } + + /** + * @return Array of Component structs with underlying tokens rates for the given token. + * @dev Implementation of TokenAdapter abstract contract function. + */ + function getComponents(address market) external view override returns (Component[] memory) { + address token = PendleMarket(market).token(); + address xyt = PendleMarket(market).xyt(); + uint256 totalSupply = ERC20(market).totalSupply(); + (uint256 xytBalance, ,uint256 tokenBalance, , ) = PendleMarket(market).getReserves(); + + Component[] memory components = new Component[](2); + + components[0] = Component({ + token: xyt, + tokenType: "ERC20", + rate: totalSupply == 0 ? 0 : xytBalance * 1e18 / totalSupply + }); + components[1] = Component({ + token: token, + tokenType: "ERC20", + rate: totalSupply == 0 ? 0 : tokenBalance * 1e18 / totalSupply + }); + + return components; + } + + function getMarketName(address market) internal view returns (string memory) { + return string( + abi.encodePacked( + getSymbol(PendleMarket(market).xyt()), + "/", + getSymbol(PendleMarket(market).token()), + " Market" + ) + ); + } + + function getSymbol(address token) internal view returns (string memory) { + (, bytes memory returnData) = token.staticcall( + abi.encodeWithSelector(ERC20(token).symbol.selector) + ); + + if (returnData.length == 32) { + return convertToString(abi.decode(returnData, (bytes32))); + } else { + return abi.decode(returnData, (string)); + } + } + + /** + * @dev Internal function to convert bytes32 to string and trim zeroes. + */ + function convertToString(bytes32 data) internal pure returns (string memory) { + uint256 length = 0; + bytes memory result; + + for (uint256 i = 0; i < 32; i++) { + if (data[i] != byte(0)) { + length++; + } + } + + result = new bytes(length); + + for (uint256 i = 0; i < length; i++) { + result[i] = data[i]; + } + + return string(result); + } +} diff --git a/contracts/adapters/pendle/PendleStakingAdapter.sol b/contracts/adapters/pendle/PendleStakingAdapter.sol new file mode 100644 index 00000000..f1e3b642 --- /dev/null +++ b/contracts/adapters/pendle/PendleStakingAdapter.sol @@ -0,0 +1,53 @@ +// Copyright (C) 2020 Zerion Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity 0.6.5; +pragma experimental ABIEncoderV2; + +import { ProtocolAdapter } from "../ProtocolAdapter.sol"; + + +/** + * @dev Proxy contract interface for calculating liquidity mining rewards. + * Only the functions required for PendleStakingZerionProxy contract are added. + */ +interface PendleStakingZerionProxy { + function earned(address user) external view returns (uint256); +} + +/** + * @title Adapter for Pendle Finance protocol. + * @dev Implementation of ProtocolAdapter interface. + * @author Anton Buenavista + */ +contract PendleStakingAdapter is ProtocolAdapter { + + string public constant override adapterType = "Asset"; + + string public constant override tokenType = "ERC20"; + + address internal constant PENDLE = 0x808507121B80c02388fAd14726482e061B8da827; + address internal constant STAKING_WRAPPER = 0x3cFfEd42e0BD6d45894283d90cF3F75e7CA55855; + + /** + * @return Amount of PENDLE rewards after staking for a given account. + * @dev Implementation of ProtocolAdapter interface function. + */ + function getBalance(address token, address account) external view override returns (uint256) { + if (token == PENDLE) { + return PendleStakingZerionProxy(STAKING_WRAPPER).earned(account); + } + } +} diff --git a/test/PendleMarketAdapter.js b/test/PendleMarketAdapter.js new file mode 100755 index 00000000..103a9383 --- /dev/null +++ b/test/PendleMarketAdapter.js @@ -0,0 +1,99 @@ +import displayToken from './helpers/displayToken'; + +const AdapterRegistry = artifacts.require('AdapterRegistry'); +const ProtocolAdapter = artifacts.require('PendleMarketAdapter'); +const TokenAdapter = artifacts.require('PendleMarketTokenAdapter'); +const ERC20TokenAdapter = artifacts.require('ERC20TokenAdapter'); + +contract('PendleMarketTokenAdapter', () => { + const ytUSDCMarket = '0x8315BcBC2c5C1Ef09B71731ab3827b0808A2D6bD'; + const ytUSDCAddress = '0xcDb5b940E95C8632dEcDc806B90dD3fC44E699fE'; + const usdcAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; + // Random address with positive balances + const testAddress = '0x76A16d9325E9519Ef1819A4e7d16B168956f325F'; + + let accounts; + let adapterRegistry; + let protocolAdapterAddress; + let tokenAdapterAddress; + let erc20TokenAdapterAddress; + const PENDLE_LPT = [ + ytUSDCMarket, + 'YT-aUSDC-29DEC2022/USDC Market', + 'PENDLE-LPT', + '18', + ]; + const YT = [ + ytUSDCAddress, + 'YT Aave interest bearing USDC 29DEC2022', + 'YT-aUSDC-29DEC2022', + '6', + ]; + const USDC = [ + usdcAddress, + 'USD Coin', + 'USDC', + '6', + ]; + + beforeEach(async () => { + accounts = await web3.eth.getAccounts(); + await ProtocolAdapter.new({ from: accounts[0] }) + .then((result) => { + protocolAdapterAddress = result.address; + }); + await TokenAdapter.new({ from: accounts[0] }) + .then((result) => { + tokenAdapterAddress = result.address; + }); + await ERC20TokenAdapter.new({ from: accounts[0] }) + .then((result) => { + erc20TokenAdapterAddress = result.address; + }); + await AdapterRegistry.new({ from: accounts[0] }) + .then((result) => { + adapterRegistry = result.contract; + }); + await adapterRegistry.methods.addProtocols( + ['Pendle Market'], + [[ + 'Mock Protocol Name', + 'Mock protocol description', + 'Mock website', + 'Mock icon', + '0', + ]], + [[ + protocolAdapterAddress, + ]], + [[[ + ytUSDCMarket, + ]]], + ) + .send({ + from: accounts[0], + gas: '1000000', + }); + await adapterRegistry.methods.addTokenAdapters( + ['ERC20', 'PendleMarket LP token'], + [erc20TokenAdapterAddress, tokenAdapterAddress], + ) + .send({ + from: accounts[0], + gas: '1000000', + }); + }); + + it('should return correct balances', async () => { + await adapterRegistry.methods['getBalances(address)'](testAddress) + .call() + .then((result) => { + displayToken(result[0].adapterBalances[0].balances[0].base); + displayToken(result[0].adapterBalances[0].balances[0].underlying[0]); + displayToken(result[0].adapterBalances[0].balances[0].underlying[1]); + assert.deepEqual(result[0].adapterBalances[0].balances[0].base.metadata, PENDLE_LPT); + assert.deepEqual(result[0].adapterBalances[0].balances[0].underlying[0].metadata, YT); + assert.deepEqual(result[0].adapterBalances[0].balances[0].underlying[1].metadata, USDC); + }); + }); +}); diff --git a/test/PendleStakingAdapter.js b/test/PendleStakingAdapter.js new file mode 100644 index 00000000..ae6ff10b --- /dev/null +++ b/test/PendleStakingAdapter.js @@ -0,0 +1,68 @@ +import displayToken from './helpers/displayToken'; + +const AdapterRegistry = artifacts.require('AdapterRegistry'); +const ProtocolAdapter = artifacts.require('PendleStakingAdapter'); +const ERC20TokenAdapter = artifacts.require('ERC20TokenAdapter'); + +contract('PendleStakingAdapter', () => { + const pendleAddress = '0x808507121B80c02388fAd14726482e061B8da827'; + // Random address with positive balances + const testAddress = '0xf8F26686F1275E5AA23A82c29079C68d3De4D3b4'; + + let accounts; + let adapterRegistry; + let protocolAdapterAddress; + let erc20TokenAdapterAddress; + + beforeEach(async () => { + accounts = await web3.eth.getAccounts(); + await ProtocolAdapter.new({ from: accounts[0] }) + .then((result) => { + protocolAdapterAddress = result.address; + }); + await ERC20TokenAdapter.new({ from: accounts[0] }) + .then((result) => { + erc20TokenAdapterAddress = result.address; + }); + await AdapterRegistry.new({ from: accounts[0] }) + .then((result) => { + adapterRegistry = result.contract; + }); + await adapterRegistry.methods.addProtocols( + ['Pendle'], + [[ + 'Mock Protocol Name', + 'Mock protocol description', + 'Mock website', + 'Mock icon', + '0', + ]], + [[ + protocolAdapterAddress, + ]], + [[[ + pendleAddress, + ]]], + ) + .send({ + from: accounts[0], + gas: '1000000', + }); + await adapterRegistry.methods.addTokenAdapters( + ['ERC20'], + [erc20TokenAdapterAddress], + ) + .send({ + from: accounts[0], + gas: '1000000', + }); + }); + + it('should return correct balances', async () => { + await adapterRegistry.methods['getBalances(address)'](testAddress) + .call() + .then((result) => { + displayToken(result[0].adapterBalances[0].balances[0].base); + }); + }); +});