-
Notifications
You must be signed in to change notification settings - Fork 24
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
thashimoto1998
wants to merge
9
commits into
main
Choose a base branch
from
strategy-idle
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+803
−0
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
47ea1e9
Add Idle finance strategy
thashimoto1998 03d4684
multi gov token support
thashimoto1998 a3dcaf5
fix decimal calculation
thashimoto1998 c1a84fe
refacto
thashimoto1998 8348888
fix computation sequence
thashimoto1998 5282edb
minor fix
thashimoto1998 bef0d83
add idle strategy test
thashimoto1998 ac26738
fix access modifier
thashimoto1998 ce0ebc7
add unregister gov token event
thashimoto1998 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
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) | ||
); | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 plaintokenPrice
. 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)There was a problem hiding this comment.
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 )There was a problem hiding this comment.
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 thegetReedmPrice
with above ref method. Thank you for your advice!There was a problem hiding this comment.
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 👍