Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Idle finance strategy #99

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,15 @@ REPORT_GAS=false
# ROLLUP_OPERATOR=

# UNISWAP_ROUTER=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
# SUSHISWAP_ROUTER=0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F
# WETH=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
# WBTC=0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599
# DAI=0x6b175474e89094c44da98b954eedeac495271d0f
# USDC=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
# USDT=0xdac17f958d2ee523a2206206994597c13d831ec7
# SUSD=0x57ab1ec28d129707052df4df418d58a2d46d5f51
# BUSD=0x4fabb145d64652a948d72533023f6e7a623c7c53
# TUSD=0x0000000000085d4780B73119b644AE5ecd22b376

# COMPOUND_CETH=0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5
# COMPOUND_CDAI=0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643
Expand Down Expand Up @@ -93,6 +96,17 @@ REPORT_GAS=false
# CURVE_STETH_LPTOKEN=0x06325440D014e39736583c165C2963BA99fAf14E
# CURVE_STETH_GAUGE=0x182B723a58739a9c974cFDB385ceaDb237453c28

# IDLE_DAI_BEST_YIELD=0x3fE7940616e5Bc47b0775a0dccf6237893353bB4
# IDLE_USDC_BEST_YIELD=0x5274891bEC421B39D23760c04A6755eCB444797C
# IDLE_USDT_BEST_YIELD=0xF34842d05A1c888Ca02769A633DF37177415C2f8
# IDLE_SUSD_BEST_YIELD=0xF52CDcD458bf455aeD77751743180eC4A595Fd3F
# IDLE_TUSD_BEST_YIELD=0xc278041fDD8249FE4c1Aad1193876857EEa3D68c
# IDLE_WBTC_BEST_YIELD=0x8C81121B15197fA0eEaEE1DC75533419DcfD3151
# IDLE_WETH_BEST_YIELD=0xC8E6CA6E96a326dC448307A5fDE90a0b21fd7f80
# IDLE_DAI_RISK_ADJUSTED=0xa14eA0E11121e6E951E87c66AFe460A00BCD6A16
# IDLE_USDC_RISK_ADJUSTED=0x3391bc034f2935eF0E1e41619445F998b2680D35
# IDLE_USDT_RISK_ADJUSTED=0x28fAc5334C9f7262b3A3Fe707e250E01053e07b5

# IMPERSONATED_DEPLOYER=0xab5801a7d398351b8be11c439e05c5b3259aec9b
# ETH_FUNDER=0xab5801a7d398351b8be11c439e05c5b3259aec9b
# DAI_FUNDER=0xf977814e90da44bfa03b6295a0616a897441acec
Expand All @@ -104,6 +118,7 @@ REPORT_GAS=false
# COMPOUND_COMP_FUNDER=0xbe0eb53f46cd790cd13851d5eff43d12404d33e8
# CURVE_CRV_FUNDER=0xf977814e90da44bfa03b6295a0616a897441acec
# AAVE_AAVE_FUNDER=0xbe0eb53f46cd790cd13851d5eff43d12404d33e8
# IDLE_IDLE_FUNDER=0x2bc44f6c34d9200c258ac07a56d81963d4f92c4c

# DUMMY_ASSET=
# DUMMY_FUNDER=
Expand Down
59 changes: 59 additions & 0 deletions contracts/strategies/idle/GovTokenRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract GovTokenRegistry is Ownable {
// Array of governance token addresses
// Governance tokens are ditributed by Idle finance
address[] public govTokens;

event GovTokenRegistered(address govTokenAddress);
event GovTokenUnregistered(address govTokenAddress);

constructor(
address _comp,
address _idle,
address _aave
){
govTokens.push(_comp);
govTokens.push(_idle);
govTokens.push(_aave);
}

function getGovTokens() public view returns (address[] memory) {
return govTokens;
}

function getGovTokensLength() public view returns (uint) {
return govTokens.length;
}

/**
* @notice Register a governance token which can swap on sushiswap
* @param _govToken The governance token address
*/
function registerGovToken(address _govToken) external onlyOwner {
require(_govToken != address(0), "Invalid governance token");
govTokens.push(_govToken);

emit GovTokenRegistered(_govToken);
}

/**
* @notice Unregister a govenance token when Idle finance does not support token
* @param _govToken The governance token address
*/
function unregisterGovToken(address _govToken) external onlyOwner {
require(_govToken != address(0), "Invalid governance token");
for (uint i = 0; i < govTokens.length; i++) {
if (govTokens[i] == _govToken) {
govTokens[i] = govTokens[govTokens.length-1];
delete govTokens[govTokens.length-1];

emit GovTokenUnregistered(_govToken);
}
}
}
}
246 changes: 246 additions & 0 deletions contracts/strategies/idle/StrategyIdleLendingPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./GovTokenRegistry.sol";

import "../interfaces/IStrategy.sol";
import "../interfaces/idle/IIdleToken.sol";
import "../interfaces/aave/IStakedAave.sol";
import "../interfaces/uniswap/IUniswapV2.sol";

/**
* Deposits ERC20 token into Idle Lending Pool V4. Holds IdleErc20(e.g. IdleDAI, IdleUSDC).
*/
contract StrategyIdleLendingPool is IStrategy, Ownable {
using SafeERC20 for IERC20;
using Address for address;
using SafeMath for uint256;

// Governance token registry
GovTokenRegistry govTokenRegistry;

// The address of Idle Lending Pool(e.g. IdleDAI, IdleUSDC)
address public iToken;

// Info of supplying erc20 token to Aave lending pool
// The symbol of the supplying token
string public symbol;
// The address of supplying token (e.g. DAI, USDT)
address public supplyToken;
// Supplying token decimals
uint8 public decimals;

// The address of Aave StakedAave contract
address public stakedAave;

address public weth;
address public sushiswap;

address public controller;

uint256 constant public FULL_ALLOC = 100000;

constructor(
address _iToken,
string memory _symbol,
address _supplyToken,
uint8 _decimals,
address _govTokenRegistryAddress,
address _stakedAave,
address _weth,
address _sushiswap,
address _controller
) {
iToken = _iToken;
symbol = _symbol;
supplyToken = _supplyToken;
decimals = _decimals;
govTokenRegistry = GovTokenRegistry(_govTokenRegistryAddress);
stakedAave = _stakedAave;
weth = _weth;
sushiswap = _sushiswap;
controller = _controller;
}

/**
* @dev Require that the caller must be an EOA account to avoid flash loans.
*/
modifier onlyEOA() {
require(msg.sender == tx.origin, "Not EOA");
_;
}

function getAssetAddress() external view override returns (address) {
return supplyToken;
}

function harvest() external override onlyEOA {
// Claim governance tokens without redeeming supply token
IIdleToken(iToken).redeemIdleToken(uint256(0));

harvestAAVE();
swapGovTokensToSupplyToken();

// Deposit obtained supply token to Idle Lending Pool
uint256 obtainedSupplyTokenAmount = IERC20(supplyToken).balanceOf(address(this));
IERC20(supplyToken).safeIncreaseAllowance(iToken, obtainedSupplyTokenAmount);
IIdleToken(iToken).mintIdleToken(obtainedSupplyTokenAmount, false, address(0));
}

function syncBalance() external view override returns (uint256) {
uint256 iTokenPrice = IIdleToken(iToken).tokenPrice();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I'm one of the founders and lead dev of Idle finance. I stumbled upon this integration, and I would like to suggest in general to always use tokenPriceWithFee instead of the plain tokenPrice. See here for more info https://developers.idle.finance/methods/tokenpricewithfee . In this way you can calculate your 'real' balance (ie with fee already counted, so you will have a net token price)

Copy link

@bugduino bugduino Aug 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this way you can also substitute the getRedeemPrice potentially (Here the ref of the method https://github.com/Idle-Labs/idle-contracts/blob/develop/contracts/IdleTokenGovernance.sol#L340 )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, bugduino. L2 finance will also integrate IDLE risk adjusted token which does not have tokenPriceWithFee method so I replace the getReedmPrice with above ref method. Thank you for your advice!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok ok, sorry was thinking about the Best yield! Nice catch then 👍

uint256 iTokenBalance = IERC20(iToken).balanceOf(address(this));
uint256 supplyTokenBalance = iTokenBalance.mul(iTokenPrice).div(10**decimals).div(10**(18 - decimals));
return supplyTokenBalance;
}

function aggregateCommit(uint256 _supplyTokenAmount) external override {
require(msg.sender == controller, "Not controller");
require(_supplyTokenAmount > 0, "Nothing to commit");

// Pull supply token from Controller
IERC20(supplyToken).safeTransferFrom(msg.sender, address(this), _supplyTokenAmount);

// Deposit supply token to Idle Lending Pool
IERC20(supplyToken).safeIncreaseAllowance(iToken, _supplyTokenAmount);
IIdleToken(iToken).mintIdleToken(_supplyTokenAmount, false, address(0));

emit Committed(_supplyTokenAmount);
}

function aggregateUncommit(uint256 _supplyTokenAmount) external override {
require(msg.sender == controller, "Not controller");
require(_supplyTokenAmount > 0, "Nothing to uncommit");

// Redeem supply token amount + interests and claim governance tokens
// When `harvest` function is called, this contract lend obtained governance token to save gas
uint256 iTokenRedeemPrice = getRedeemPrice();
uint256 iTokenBurnAmount = _supplyTokenAmount.mul(10**(decimals)).div(iTokenRedeemPrice).mul(10**(18 - decimals));
IIdleToken(iToken).redeemIdleToken(iTokenBurnAmount);

// Transfer supply token to Controller
uint256 supplyTokenBalance = IERC20(supplyToken).balanceOf(address(this));
IERC20(supplyToken).safeTransfer(msg.sender, supplyTokenBalance);

emit UnCommitted(_supplyTokenAmount);
}

function setController(address _controller) external onlyOwner {
emit ControllerChanged(controller, _controller);
controller = _controller;
}

// Refer to IdleTokenHelper.sol (https://github.com/emilianobonassi/idle-token-helper/blob/master/IdleTokenHelper.sol)
function getRedeemPrice() public view returns (uint256) {
/*
* As per https://github.com/Idle-Labs/idle-contracts/blob/ad0f18fef670ea6a4030fe600f64ece3d3ac2202/contracts/IdleTokenGovernance.sol#L878-L900
*
* Price on minting is currentPrice
* Price on redeem must consider the fee
*
* Below the implementation of the following redeemPrice formula
*
* redeemPrice := underlyingAmount/idleTokenAmount
*
* redeemPrice = currentPrice * (1 - scaledFee * ΔP%)
*
* where:
* - scaledFee := fee/FULL_ALLOC
* - ΔP% := 0 when currentPrice < userAvgPrice (no gain) and (currentPrice-userAvgPrice)/currentPrice
*
* n.b: gain := idleTokenAmount * ΔP% * currentPrice
*/
uint256 userAvgPrice = IIdleToken(iToken).userAvgPrices(address(this));
uint256 currentPrice = IIdleToken(iToken).tokenPrice();

// When no deposits userAvgPrice is 0 equiv currentPrice
// and in the case of
uint256 redeemPrice;
if (userAvgPrice == 0 || currentPrice < userAvgPrice) {
redeemPrice = currentPrice;
} else {
uint256 fee = IIdleToken(iToken).fee();

redeemPrice = ((currentPrice.mul(FULL_ALLOC))
.sub(
fee.mul(
currentPrice.sub(userAvgPrice)
)
)).div(FULL_ALLOC);
}

return redeemPrice;
}

function harvestAAVE() private {
// Idle finance transfer stkAAVE to this contract
// Activates the cooldown period if not already activated
uint256 stakedAaveBalance = IERC20(stakedAave).balanceOf(address(this));
if (stakedAaveBalance > 0 && IStakedAave(stakedAave).stakersCooldowns(address(this)) == 0) {
IStakedAave(stakedAave).cooldown();
}

// Claims the AAVE staking rewards
uint256 stakingRewards = IStakedAave(stakedAave).getTotalRewardsBalance(address(this));
if (stakingRewards > 0) {
IStakedAave(stakedAave).claimRewards(address(this), stakingRewards);
}

// Redeems staked AAVE if possible
uint256 cooldownStartTimestamp = IStakedAave(stakedAave).stakersCooldowns(address(this));
if (
stakedAaveBalance > 0 &&
block.timestamp > cooldownStartTimestamp.add(IStakedAave(stakedAave).COOLDOWN_SECONDS()) &&
block.timestamp <=
cooldownStartTimestamp.add(IStakedAave(stakedAave).COOLDOWN_SECONDS()).add(
IStakedAave(stakedAave).UNSTAKE_WINDOW()
)
) {
IStakedAave(stakedAave).redeem(address(this), stakedAaveBalance);
}
}

function swapGovTokensToSupplyToken() private {
uint govTokenLength = govTokenRegistry.getGovTokensLength();
address[] memory govTokens = govTokenRegistry.getGovTokens();
for(uint32 i = 0; i < govTokenLength; i++) {
uint256 govTokenBalance = IERC20(govTokens[i]).balanceOf(address(this));
if (govTokenBalance > 0) {
IERC20(govTokens[i]).safeIncreaseAllowance(sushiswap, govTokenBalance);
if (supplyToken != weth) {
address[] memory paths = new address[](3);
paths[0] = govTokens[i];
paths[1] = weth;
paths[2] = supplyToken;

IUniswapV2(sushiswap).swapExactTokensForTokens(
govTokenBalance,
uint256(0),
paths,
address(this),
block.timestamp.add(1800)
);
} else {
address[] memory paths = new address[](2);
paths[0] = govTokens[i];
paths[1] = weth;

IUniswapV2(sushiswap).swapExactTokensForTokens(
govTokenBalance,
uint256(0),
paths,
address(this),
block.timestamp.add(1800)
);
}
}
}
}
}
Loading